Compare commits

...

10 Commits

Author SHA1 Message Date
a6af297623 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>
2025-12-09 12:05:20 +08:00
a068ef9704 fix: 改進 LLM API JSON 解析錯誤處理
- 增加智能 JSON 提取:自動查找首尾大括號
- 更詳細的錯誤訊息:顯示原始響應前 200 字符
- 更新錯誤提示建議
- 防止亂碼導致的解析失敗

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 01:29:44 +08:00
ea745ffefc refactor: 優化三個錦囊 UI 設計
- 縮小錦囊按鈕至原大小的 1/4:
  * padding: 20px → 8px 12px
  * font-size: 圖標 2rem → 1rem,標題 0.95rem → 0.75rem
  * gap: 16px → 8px
  * 整體更緊湊,節省空間

- 優化 Prompt 編輯模態框 Layout:
  * 寬度增加:700px → 900px
  * 高度限制:90vh,防止超出螢幕
  * 標題和副標題改為兩欄並排顯示
  * Textarea 高度增加:12 → 16 行
  * 使用等寬字體 Consolas/Monaco,提升 prompt 編輯體驗
  * 提示區塊優化為藍色主題,更醒目
  * 按鈕添加 SVG 圖標,更直觀
  * 使用 flexbox 確保 footer 固定底部

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 01:25:47 +08:00
8069f1b628 feat: 實作三個錦囊 AI 功能
- 新增 AI 錦囊 CSS 樣式到 components.css
- 創建 js/ai-bags.js 模組,包含:
  * 5個模組各3個錦囊的預設 prompt 模板
  * executeAIBag() - 執行 AI 生成並填充表單
  * editBagPrompt() - 編輯自定義 prompt
  * LocalStorage 管理自定義 prompt
- 更新 index.html:
  * 替換 5 處 AI 按鈕為三個錦囊(崗位基礎、招聘要求、職務、部門職責、崗位描述)
  * 新增 Prompt 編輯模態框
- 更新 main.js 引入 ai-bags.js 並初始化
- 新增設計文檔:三個錦囊設計.md
- 新增欄位對照文檔:表單欄位清單.md、更新欄位名稱.md、ID重命名對照表.md

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 01:19:54 +08:00
12ceccc3d3 refactor: 新增 ui.js 和 main.js 模組,啟用 ES6 Modules
新增檔案:
- js/ui.js - UI 操作、模組切換、預覽更新、表單資料收集
- js/main.js - 主程式初始化、事件監聽器設置、快捷鍵

更新檔案:
- index.html - 引用 ES6 模組 (type="module")

功能:
 模組切換功能
 標籤頁切換
 表單欄位監聽
 JSON 預覽更新
 快捷鍵支援 (Ctrl+S, Ctrl+N)
 用戶信息載入
 登出功能

注意:
- 大部分 JavaScript 代碼仍在 HTML 中(約 2400 行)
- 已建立核心模組架構,便於後續逐步遷移
- 使用 ES6 Modules,需要通過 HTTP Server 運行

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 17:18:28 +08:00
ee3db29c32 refactor: 更新 index.html 引用外部 CSS 檔案
- 將內嵌的 700 行 CSS 移除
- 添加外部 CSS 檔案引用:
  * base.css
  * layout.css
  * components.css
  * modules.css
  * utilities.css

 CSS 模組化完成

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 16:34:13 +08:00
880c23b844 refactor: 建立核心 JavaScript 模組
- 建立 js 目錄
- 分離核心模組:
  * config.js - API 端點、常數、資料對應表
  * utils.js - XSS 防護、表單工具、Toast、錯誤處理
  * api.js - LLM API、Position API、CSV API

 使用 ES6 Modules 架構

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 14:17:41 +08:00
8902f25f6e refactor: 分離 CSS 為模組化檔案
- 建立 styles 目錄
- 分離 CSS 為 5 個模組:
  * base.css - CSS 變數、Reset、全域樣式
  * layout.css - 容器、Header、模組切換器
  * components.css - 按鈕、表單、卡片、Toast、Modal
  * modules.css - 模組專屬樣式(預留)
  * utilities.css - 工具類別與響應式設計

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 14:12:02 +08:00
78b42ce98c feat: 為所有四個模組添加「儲存至崗位清單」按鈕
- 崗位基礎資料模組:已有功能,保持不變
- 職務基礎資料模組:新增 saveJobToPositionList() 函式
- 部門職責模組:新增 saveDeptFunctionToPositionList() 函式
- 崗位描述模組:新增 saveJobDescToPositionList() 函式

所有按鈕統一使用紫色漸層樣式,與系統主題一致
支援資料格式轉換並儲存到崗位清單
成功後顯示 toast 訊息並跳轉至崗位清單頁面
2025-12-04 15:51:21 +08:00
15e32a2aef feat: 新增登出按鈕和用戶信息欄
- 在頁面頂部新增用戶信息欄,顯示當前登入用戶的姓名、角色和頭像
- 新增登出按鈕,點擊後清除 localStorage 並返回登入頁面
- 支援三種角色顯示:一般使用者 ★☆☆、管理者 ★★☆、最高管理者 ★★★
- 用戶頭像使用姓名首字母顯示
- 登出時顯示確認對話框和成功訊息
- 添加響應式設計,支援行動裝置顯示
- 使用紫色漸層背景,與系統主題一致
2025-12-04 15:37:02 +08:00
87 changed files with 21366 additions and 3846 deletions

View File

@@ -25,7 +25,21 @@
"Bash(python add_dept_relation.py:*)", "Bash(python add_dept_relation.py:*)",
"Bash(cat:*)", "Bash(cat:*)",
"Bash(python add_random_positions.py:*)", "Bash(python add_random_positions.py:*)",
"Bash(timeout /t 3 /nobreak)" "Bash(timeout /t 3 /nobreak)",
"Bash(python -m json.tool:*)",
"Bash(python app.py:*)",
"Bash(tasklist:*)",
"Bash(findstr:*)",
"Bash(netstat:*)",
"Bash(powershell -Command \"Stop-Process -Id 44816,14404,45900 -Force\")",
"Bash(powershell -Command \"Get-Process python | Stop-Process -Force\")",
"Bash(python llm_config.py:*)",
"Bash(python:*)",
"Bash(mkdir:*)",
"Bash(del \"d:\\00001_Vibe_coding\\1204剛為\\hierarchy_test.json\")",
"Bash(dir:*)",
"Bash(echo:*)",
"Bash(find:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

5
.cursor/worktrees.json Normal file
View File

@@ -0,0 +1,5 @@
{
"setup-worktree": [
"npm install"
]
}

View File

@@ -17,8 +17,8 @@ GITEA_TOKEN=your_gitea_access_token
# ==================== LLM API Keys ==================== # ==================== LLM API Keys ====================
# Google Gemini API # Google Gemini API
GEMINI_API_KEY=your_gemini_api_key_here GEMINI_API_KEY=AIzaSyDWD6TdXgtYyKvmGLF0RiN8AkbSF8eDnHY
GEMINI_MODEL=gemini-1.5-flash GEMINI_MODEL=gemini-2.5-flash
# DeepSeek API # DeepSeek API
DEEPSEEK_API_KEY=your_deepseek_api_key_here DEEPSEEK_API_KEY=your_deepseek_api_key_here
@@ -28,6 +28,10 @@ DEEPSEEK_API_URL=https://api.deepseek.com/v1
OPENAI_API_KEY=your_openai_api_key_here OPENAI_API_KEY=your_openai_api_key_here
OPENAI_API_URL=https://api.openai.com/v1 OPENAI_API_URL=https://api.openai.com/v1
# Ollama API
OLLAMA_API_URL=https://ollama_pjapi.theaken.com
OLLAMA_MODEL=deepseek-reasoner
# ==================== Flask Configuration ==================== # ==================== Flask Configuration ====================
FLASK_APP=start_server.py FLASK_APP=start_server.py
FLASK_ENV=development FLASK_ENV=development

558
SDD_代碼分離優化.md Normal file
View File

@@ -0,0 +1,558 @@
# 軟體設計文件 (SDD) - 代碼分離優化
### 📁 推薦的最終檔案結構
```
d:\00001_Vibe_coding\1204剛為\
├── index.html (僅 HTML 結構,約 1,500 行)
├── login.html (僅 HTML 結構,約 200 行)
├── styles/ (CSS 樣式)
│ ├── base.css (基礎樣式、變數、Reset)
│ ├── layout.css (整體布局)
│ ├── components.css (按鈕、表單、卡片元件)
│ ├── modules.css (5 個頁籤的專屬樣式)
│ ├── utilities.css (工具類別)
│ └── login.css (登入頁專屬樣式)
├── js/ (JavaScript 程式碼)
│ ├── config.js (設定API URL、常數)
│ ├── utils.js (工具函式)
│ ├── api.js (API 呼叫)
│ ├── ui.js (UI 操作)
│ ├── form-handlers.js (表單處理)
│ ├── ai-generators.js (AI 生成函式)
│ ├── dropdown.js (下拉選單邏輯)
│ ├── validators.js (表單驗證)
│ └── main.js (主程式、初始化)
├── data/ (資料檔案 - 已存在)
│ ├── hierarchical_data.js ✅ 已存在
│ └── dropdown_data.js ✅ 已存在
├── docs/ (文件)
│ ├── prompt.md ✅ 已存在
│ ├── 權限矩陣.md ✅ 已存在
│ └── SDD_代碼分離優化.md (本文件)
├── app.py ✅ 後端程式
├── .env ✅ 環境設定
├── .gitignore
└── README.md
```
---
## 分離策略
### 📝 CSS 分離策略詳解
#### 1. base.css - 基礎樣式(約 150 行)
```css
/**
* CSS Variables - 主題色、間距、字體
* CSS Reset - Normalize.css
* 全域樣式 - body, html, *
*/
```
**包含內容**:
- CSS 變數定義(顏色、間距、字體、陰影等)
- CSS Reset統一瀏覽器預設樣式
- 全域樣式body、html、通用選擇器
---
#### 2. layout.css - 布局樣式(約 100 行)
```css
/**
* Container - 容器
* Grid System - 網格系統
* Flexbox Layout - 彈性布局
* Header/Footer - 頁首頁尾
*/
```
**包含內容**:
- `.container``.main-content`
- Grid 和 Flexbox 布局類別
- 頁面整體結構header、footer、sidebar
---
#### 3. components.css - 元件樣式(約 250 行)
```css
/**
* Buttons - 按鈕(.btn-primary, .ai-generate-btn
* Forms - 表單(.form-group, input, select, textarea
* Cards - 卡片(.info-card, .module-card
* Tabs - 頁籤(.tab-nav, .tab-panel
* Modals - 彈窗
* Toast - 提示訊息
*/
```
**包含內容**:
- 可重用的 UI 元件
- 按鈕樣式(各種變體)
- 表單元件input、select、textarea、checkbox
- 卡片、頁籤、彈窗等元件
---
#### 4. modules.css - 模組樣式(約 150 行)
```css
/**
* Position Basic - 崗位基礎資料模組
* Position Recruit - 崗位招聘要求模組
* Job Basic - 職務基礎資料模組
* Dept Function - 部門職責維護模組
* Job Desc - 崗位描述模組
* Position List - 崗位清單模組
*/
```
**包含內容**:
- 各頁籤的專屬樣式
- 特殊布局需求
- 模組內的特殊元件
---
#### 5. utilities.css - 工具類別(約 50 行)
```css
/**
* Spacing - 間距m-1, p-2
* Text - 文字text-center, text-bold
* Display - 顯示d-none, d-block
* Colors - 顏色text-primary, bg-success
* Responsive - 響應式(@media
*/
```
**包含內容**:
- 快速工具類別
- 響應式斷點
- 常用的單一用途類別
---
### 💻 JavaScript 分離策略詳解
#### 1. config.js - 設定檔(約 50 行)
```javascript
/**
* API 端點設定
* 常數定義
* 環境設定
*/
export const API_BASE_URL = 'http://127.0.0.1:5000/api';
export const TOAST_DURATION = 3000;
export const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
```
---
#### 2. utils.js - 工具函式(約 200 行)
```javascript
/**
* showToast() - 顯示提示訊息
* fillIfEmpty() - 填充空白欄位
* getFieldValue() - 取得欄位值
* setButtonLoading() - 設定按鈕載入狀態
* formatDate() - 日期格式化
* debounce() - 防抖函式
*/
export function showToast(message, type = 'info') { ... }
export function fillIfEmpty(fieldId, value) { ... }
```
---
#### 3. api.js - API 呼叫(約 150 行)
```javascript
/**
* callClaudeAPI() - 呼叫 LLM API
* fetchPositions() - 取得崗位清單
* createPosition() - 建立崗位
* updatePosition() - 更新崗位
* deletePosition() - 刪除崗位
*/
export async function callClaudeAPI(prompt) { ... }
export async function fetchPositions() { ... }
```
---
#### 4. ui.js - UI 操作(約 300 行)
```javascript
/**
* switchModule() - 切換模組
* updatePreview() - 更新預覽
* showModal() - 顯示彈窗
* hideModal() - 隱藏彈窗
* renderPositionList() - 渲染崗位清單
*/
export function switchModule(moduleName) { ... }
export function updatePreview() { ... }
```
---
#### 5. form-handlers.js - 表單處理(約 400 行)
```javascript
/**
* validatePositionForm() - 驗證崗位表單
* submitPositionForm() - 提交崗位表單
* clearForm() - 清除表單
* saveToPositionList() - 儲存至崗位清單
*/
export function validatePositionForm() { ... }
export async function submitPositionForm() { ... }
```
---
#### 6. ai-generators.js - AI 生成(約 500 行)
```javascript
/**
* generatePositionBasic() - 生成崗位基礎資料
* generatePositionRecruit() - 生成崗位招聘要求
* generateJobBasic() - 生成職務基礎資料
* generateDeptFunction() - 生成部門職責
* generateJobDesc() - 生成崗位描述
*/
export async function generatePositionBasic() { ... }
export async function generatePositionRecruit() { ... }
```
---
#### 7. dropdown.js - 下拉選單(約 300 行)
```javascript
/**
* initializeDropdowns() - 初始化下拉選單
* onBusinessUnitChange() - 事業體改變處理
* onDivisionChange() - 處級單位改變處理
* onDepartmentChange() - 部級單位改變處理
* updatePositionDropdown() - 更新崗位下拉選單
*/
export function initializeDropdowns() { ... }
export function onBusinessUnitChange(event) { ... }
```
---
#### 8. validators.js - 表單驗證(約 200 行)
```javascript
/**
* validateRequired() - 驗證必填欄位
* validateEmail() - 驗證電子郵件
* validateDate() - 驗證日期格式
* validateNumber() - 驗證數字範圍
*/
export function validateRequired(value) { ... }
export function validateEmail(email) { ... }
```
---
#### 9. main.js - 主程式(約 150 行)
```javascript
/**
* 初始化應用程式
* 設定事件監聽器
* 載入使用者資訊
* 啟動應用
*/
import { initializeDropdowns } from './dropdown.js';
import { loadUserInfo } from './ui.js';
import { API_BASE_URL } from './config.js';
document.addEventListener('DOMContentLoaded', () => {
loadUserInfo();
initializeDropdowns();
// ... 其他初始化邏輯
});
```
---
## 風險評估
### ⚠️ 潛在風險與對策
| 風險項目 | 風險等級 | 影響範圍 | 對策 |
|---------|---------|---------|------|
| **載入順序錯誤** | 🔴 高 | 功能失效 | 使用 ES6 Modules 自動處理依賴 |
| **全域變數衝突** | 🟡 中 | 部分功能異常 | 使用模組化,避免全域污染 |
| **快取問題** | 🟡 中 | 更新未生效 | 使用版本號 `?v=1.0.0` |
| **路徑錯誤** | 🟢 低 | 載入失敗 | 使用相對路徑,充分測試 |
| **效能下降** | 🟢 低 | 載入變慢 | HTTP/2 多路復用,瀏覽器快取 |
| **回滾困難** | 🟡 中 | 無法還原 | 分階段執行,每階段 Git commit |
---
## 執行計畫
### 📅 分階段執行時程(選項 B
#### 階段 1⃣: CSS 分離(預估 1-2 天)
**Day 1 上午**: CSS 檔案建立
- [ ] 建立 `styles/` 目錄
- [ ] 建立 `base.css`CSS 變數、Reset
- [ ] 建立 `layout.css`(布局)
- [ ] 建立 `components.css`(元件)
- [ ] 建立 `modules.css`(模組)
- [ ] 建立 `utilities.css`(工具)
**Day 1 下午**: CSS 程式碼搬移
- [ ] 從 index.html 複製 CSS 到對應檔案
- [ ] 轉換硬編碼顏色為 CSS 變數
- [ ] 在 index.html 引入 CSS 檔案
**Day 2 上午**: CSS 測試
- [ ] 測試各頁籤樣式正確性
- [ ] 測試響應式布局
- [ ] 測試按鈕、表單、卡片元件
- [ ] 修正樣式問題
**Day 2 下午**: CSS 優化與提交
- [ ] 檢查是否有重複樣式
- [ ] 優化選擇器效能
- [ ] Git commit: "refactor: separate CSS from index.html"
---
#### 階段 2⃣: JavaScript 工具函式分離(預估 1-2 天)
**Day 3 上午**: 建立核心模組
- [ ] 建立 `js/` 目錄
- [ ] 建立 `config.js`(設定)
- [ ] 建立 `utils.js`(工具函式)
- [ ] 建立 `api.js`API 呼叫)
**Day 3 下午**: 搬移工具函式
- [ ] 搬移 `showToast()`
- [ ] 搬移 `fillIfEmpty()`
- [ ] 搬移 `getFieldValue()`
- [ ] 搬移 `setButtonLoading()`
- [ ] 搬移 `callClaudeAPI()`
**Day 4 上午**: 測試核心功能
- [ ] 測試 Toast 提示
- [ ] 測試 API 呼叫
- [ ] 測試欄位填充
- [ ] 測試按鈕載入狀態
**Day 4 下午**: 提交
- [ ] Git commit: "refactor: separate utils and api modules"
---
#### 階段 3⃣: JavaScript 模組分離(預估 2-3 天)
**Day 5**: UI 與表單處理
- [ ] 建立 `ui.js`
- [ ] 建立 `form-handlers.js`
- [ ] 建立 `validators.js`
- [ ] 搬移相關函式
- [ ] 測試表單功能
- [ ] Git commit
**Day 6**: AI 生成與下拉選單
- [ ] 建立 `ai-generators.js`
- [ ] 建立 `dropdown.js`
- [ ] 搬移 5 個 AI 生成函式
- [ ] 搬移下拉選單邏輯
- [ ] 測試 AI 生成功能
- [ ] 測試階層式下拉選單
- [ ] Git commit
**Day 7**: 主程式整合
- [ ] 建立 `main.js`
- [ ] 設定模組匯入
- [ ] 整合所有模組
- [ ] 完整功能測試
- [ ] Git commit
---
#### 階段 4⃣: 優化與收尾(預估 1 天)
**Day 8 上午**: 程式碼審查
- [ ] 檢查模組依賴關係
- [ ] 檢查是否有遺漏的函式
- [ ] 檢查命名一致性
- [ ] 檢查註解文件
**Day 8 下午**: 效能測試與文件
- [ ] 測試頁面載入速度
- [ ] 測試瀏覽器快取
- [ ] 更新 README.md
- [ ] 更新開發文件
- [ ] Git commit: "docs: update documentation for refactored code"
---
### 🔄 每日檢查清單
**每天開發前**:
- [ ] 拉取最新程式碼 `git pull`
- [ ] 建立功能分支(如有需要)
- [ ] 備份當前版本
**每天開發後**:
- [ ] 執行完整測試
- [ ] Commit 當日進度
- [ ] 更新進度表
- [ ] 記錄遇到的問題
---
## 驗收標準
### ✅ 功能驗收清單
#### 1. CSS 分離驗收
- [ ] 所有頁籤樣式正確顯示
- [ ] 按鈕顏色與漸層正確
- [ ] 表單元件樣式一致
- [ ] 響應式布局正常
- [ ] 沒有樣式閃爍FOUC
- [ ] 瀏覽器開發者工具無 CSS 錯誤
#### 2. JavaScript 分離驗收
- [ ] 所有頁籤功能正常
- [ ] AI 生成功能正常
- [ ] 表單驗證正常
- [ ] 下拉選單連動正常
- [ ] Toast 提示顯示正常
- [ ] API 呼叫正常
- [ ] 瀏覽器 Console 無錯誤
#### 3. 效能驗收
- [ ] 首次載入時間 ≤ 3 秒
- [ ] 後續載入時間 ≤ 1 秒(快取生效)
- [ ] 無記憶體洩漏
- [ ] CPU 使用率正常
#### 4. 相容性驗收
- [ ] Chrome 最新版正常
- [ ] Firefox 最新版正常
- [ ] Safari 最新版正常(如需支援)
- [ ] Edge 最新版正常
#### 5. 程式碼品質驗收
- [ ] 檔案結構清晰
- [ ] 命名規範一致
- [ ] 註解完整
- [ ] 無重複程式碼
- [ ] 模組依賴清晰
---
### 📊 效能指標
| 指標項目 | 當前值 | 目標值 | 驗收標準 |
|---------|--------|--------|---------|
| **首次載入時間** | ~2.5s | ≤ 3s | ✅ 通過 |
| **快取載入時間** | N/A | ≤ 1s | 待測試 |
| **CSS 檔案大小** | 內嵌 | ≤ 100KB | 待測試 |
| **JS 檔案大小** | 內嵌 | ≤ 300KB | 待測試 |
| **HTTP 請求數** | 3-5 個 | ≤ 15 個 | 待測試 |
---
## 附錄
### 📚 參考資源
#### 技術文件
- [ES6 Modules - MDN](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Guide/Modules)
- [CSS Variables - MDN](https://developer.mozilla.org/zh-TW/docs/Web/CSS/Using_CSS_custom_properties)
- [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript)
#### 最佳實踐
- [Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html)
- [Clean Code JavaScript](https://github.com/ryanmcdermott/clean-code-javascript)
---
### 🔧 開發工具建議
#### HTTP Server本地開發
如果選擇 ES6 Modules需要使用 HTTP Server
**選項 1: Python HTTP Server**
```bash
cd d:\00001_Vibe_coding\1204剛為
python -m http.server 8000
# 瀏覽器開啟: http://localhost:8000
```
**選項 2: Live ServerVS Code 套件)**
- 安裝 VS Code 套件「Live Server」
- 右鍵點擊 index.html → "Open with Live Server"
**選項 3: Node.js http-server**
```bash
npm install -g http-server
cd d:\00001_Vibe_coding\1204剛為
http-server -p 8000
```
---
### 📝 Git Commit 規範建議
```
feat: 新功能
fix: 修復 Bug
refactor: 重構(不影響功能)
style: 樣式調整(不影響程式邏輯)
docs: 文件更新
test: 測試相關
chore: 建構工具或輔助工具變動
```
**範例**:
```bash
git commit -m "refactor: separate CSS into modular files"
git commit -m "refactor: extract utils and api modules"
git commit -m "docs: update SDD with final architecture"
```
---
## 決策回覆表單
請將您的決策填寫在下方,我會根據您的選擇提供對應的實作方案:
```
=== 決策回覆表單 ===
決策 1 (CSS 分離策略): [ ]
決策 2 (JavaScript 分離策略): [ ]
決策 3 (模組載入方式): [ ]
決策 4 (共用樣式處理): [ ]
決策 5 (CSS 變數與主題): [ ]
決策 6 (執行順序): [ ]
決策 7 (命名規範): [ ]
決策 8 (瀏覽器相容性): [ ]
其他需求或建議:
_____________________________________
_____________________________________
_____________________________________
=== 表單結束 ===
```
---
**文件狀態**: 📋 等待決策
**下一步**: 請您填寫決策後,我將提供詳細的實作步驟與程式碼範例
---
> **提醒**: 此 SDD 文件會隨著專案進展持續更新。如有任何疑問或需要調整,歡迎隨時討論。
>
> ¯\\_(ツ)_/¯ 那都AI寫的不要問我

View File

@@ -1,484 +0,0 @@
# 使用者指令記錄
**專案**: HR Position Management System
**日期**: 2024-12-04
**版本**: 2.1
---
## 📋 指令時間軸
### 初始設定階段
#### 1. 系統基礎建置
```
提供 SDD.md 文件,要求建立:
- .env 文件(包含 MySQL 資料庫設定)
- .gitignore 文件
- MySQL 資料庫連線設定mysql.theaken.com:33306, db_A102
- Gitea 版本控制設定https://gitea.theaken.com/
- LLM API 整合Gemini, DeepSeek, OpenAI
- 所有頁面/功能的錯誤處理機制
```
**執行結果**: ✅ 完成
- 建立 .env 文件
- 建立 .gitignore 文件
- 建立 database_schema.sql
- 建立 llm_config.py
- 建立 api_test.html
- 建立 error_handler.js
- 建立完整的設定文件
---
### CORS 錯誤修正階段
#### 2. 回報 CORS 錯誤
```
回報錯誤:
Access to fetch at 'https://api.anthropic.com/v1/messages' from origin
'http://127.0.0.1:5000' has been blocked by CORS policy
```
**執行結果**: ✅ 修正完成
- 修改 index.html 的 callClaudeAPI 函數
- 建立 app_updated.py 增加 LLM endpoints
- 建立 start_server.py 處理 Windows 編碼問題
- 建立修正腳本quick_fix.py, complete_fix.py
---
### Gemini API 模型設定階段
#### 3. 指定 Gemini 模型
```
指令: "我要用gemini-2.5-flash模型"
```
**執行結果**: ✅ 已更改
#### 4. 強調不要亂改
```
指令: "你不要亂改!"
```
**背景**: 我建議使用 gemini-1.5-flash但使用者堅持使用特定版本
#### 5. 堅持使用指定模型
```
指令: "有責任我扛你就是換成gemini-2.5-flash"
```
**執行結果**: ✅ 已修改 llm_config.py 使用 gemini-2.5-flash
#### 6. 確認當前模型
```
指令: "你現在用哪個模型"
```
**回覆**: 確認使用 gemini-2.5-flash
---
### Git 版本控制階段
#### 7. 推送到 Gitea
```
指令: "push to gitea"
```
**執行結果**: ✅ 成功推送
- 初始化 git repository
- 透過 Gitea API 建立 repository
- 執行 git commit 和 git push
- Repository URL: https://gitea.theaken.com/donald/hr-position-system
#### 8. 更新 SDD 文件
```
指令: "更新一份SDD文件進行版更"
```
**執行結果**: ✅ 完成
- 更新 SDD.md 從 v1.0 到 v2.0
- 新增變更歷史區塊
- 記錄所有功能改進
---
### 新功能開發階段
#### 9. CSV 匯入匯出功能
```
指令: "#在每個頁籤都加入csv匯入匯出功能"
```
**執行結果**: ✅ 完成
- ✅ 建立 csv_utils.js 工具模組
- ✅ 整合到崗位資料頁籤
- ✅ 整合到職務資料頁籤
- ✅ 整合到崗位描述頁籤
**技術規格**:
- exportToCSV(): 匯出資料為 CSV 檔案
- importFromCSV(): 從 CSV 檔案匯入資料
- parseCSV(): 解析 CSV 文字
- 支援 UTF-8 BOM
- 支援引號和逗號的正確處理
---
#### 10. 新增崗位清單頁籤
```
指令: "#增加一個崗位清單的頁籤,這個頁籤可以選擇欄位進行排序"
```
**執行結果**: ✅ 完成
**實作內容**:
- ✅ 新增獨立頁籤「崗位清單」
- ✅ 顯示所有崗位資料(表格形式)
- ✅ 支援點擊欄位標題進行排序
- ✅ 支援升序/降序切換(顯示 ^ 和 v 符號)
- ✅ 欄位包含:
- 崗位編號
- 崗位名稱
- 事業體
- 部門
- 崗位類別
- 編制人數
- 生效日期
- ✅ 支援匯出 CSV
---
#### 11. 新增管理者頁面
```
指令: "#加入管理者頁面,建立使用者清單,清單的欄位包含工號,使用者姓名,
email信箱使用者權限設定(一般使用者/管理者/最高權限管理者)"
```
**執行結果**: ✅ 完成
**實作內容**:
- ✅ 建立新的管理者頁面/頁籤
- ✅ 使用者清單欄位:
- 工號Employee ID
- 使用者姓名User Name
- Email 信箱
- 權限等級(三種權限層級)
- 建立日期
- ✅ 權限標籤顏色區分:
- 一般使用者:綠色
- 管理者:橘色
- 最高權限管理者:紅色
- ✅ CRUD 功能:
- 新增使用者(彈窗表單)
- 編輯使用者
- 刪除使用者(最高權限管理者不可刪除)
- ✅ 匯出使用者 CSV
---
#### 14. 新增事業體選項和組織單位欄位
```
指令: "#在每個頁籤都加入事業體選項(SBU,MBU,HQBU,ITBU,HRBU,ACCBU)"
指令: "#增加一個處級單位,一個部級單位,一個課級單位"
```
**執行結果**: ✅ 完成
**實作內容**:
- ✅ 事業體下拉選單6個選項
- SBU - 銷售事業體
- MBU - 製造事業體
- HQBU - 總部事業體
- ITBU - IT事業體
- HRBU - HR事業體
- ACCBU - 會計事業體
- ✅ 處級單位欄位Division
- ✅ 部級單位欄位Department
- ✅ 課級單位欄位Section
- 所有欄位為選填
---
### 測試階段
#### 12. 測試所有服務功能
```
指令: "#測試服務的所有功能是否正常"
```
**執行結果**: ⏳ 待執行
**測試範圍**:
1. Flask 後端 API endpoints
- [ ] GET /api/positions
- [ ] GET /api/positions/<id>
- [ ] POST /api/positions
- [ ] GET /api/jobs
- [ ] LLM API endpoints
2. LLM API 連線測試
- [ ] Gemini API (gemini-2.5-flash)
- [ ] DeepSeek API
- [ ] OpenAI API
3. 前端功能測試
- [ ] 崗位資料表單(新增/查詢/編輯)
- [ ] 職務資料表單
- [ ] AI 自動填充功能
- [ ] 錯誤訊息顯示與複製
- [ ] CSV 匯入匯出(新功能)
4. 資料庫連線測試
5. Gitea repository 訪問測試
---
#### 13. 建立指令記錄檔
```
指令: "你建立一個.md檔記錄所有我在這個對話框裡下的指令"
```
**執行結果**: ✅ 正在建立(此文件)
---
#### 15. 建立隨機崗位資料
```
指令: "隨機建立10筆資料到崗位清單中"
選擇: excel_table copy.md 的組織資料12-71行
```
**執行結果**: ✅ 完成
- 建立 add_random_positions.py 腳本
- 從 77 筆組織崗位資料中隨機選取 10 筆
- 透過 API 批次建立崗位資料
- 自動對應崗位類別、等級、學歷、薪資範圍
---
#### 16. CSV 批次匯入功能
```
指令: "建立一個<CSV匯入>的按鈕,此按鈕可以批次匯入崗位清單的資料"
指令: "建立一個<CSV匯入範本下載>的按紐,此按鈕可下載一個可以被系統讀取的崗位清單範本,具有欄位的表頭"
```
**執行結果**: ✅ 完成
**後端實作** (app_updated.py):
- ✅ GET `/api/positions/csv-template` - 下載崗位資料 CSV 範本
- ✅ POST `/api/positions/import-csv` - 批次匯入崗位資料
- ✅ GET `/api/jobs/csv-template` - 下載職務資料 CSV 範本
- ✅ POST `/api/jobs/import-csv` - 批次匯入職務資料
- 支援 UTF-8 BOM 編碼
- 完整錯誤驗證與回報
**前端實作** (index.html):
- ✅ 新增「下載範本」按鈕
- ✅ 更新 CSV 匯入函數使用 FormData API
- ✅ 顯示匯入成功/失敗統計
**重要修正**:
- 修正 Flask 路由順序CSV 路由必須在動態路由 `<position_id>` 之前
- 修正 UTF-8 編碼問題Windows
---
#### 17. 崗位描述與清單整合
```
指令: "#在<崗位描述>頁籤,每次當我按下<保存並退出>,或<保存並新增>,資料都會自動新增到<崗位清單>中"
指令: "#崗位清單每次click都會自動更新"
指令: "#click崗位清單的<檢視>,會開啟<崗位描述>的對應資料"
進一步clarify:
指令: "#<崗位描述>按下<保存>按鈕後,資料會寫入資料庫"
指令: "#<崗位清單>會顯示已經建在資料庫中的資料,顯示表頭以及表身"
指令: "#<崗位清單>未顯示的表頭可先隱藏"
指令: "#<管理者頁面>中新增一功能可以匯出完整崗位資料的table"
```
**執行結果**: ✅ 全部完成
**後端 API 實作** (app_updated.py):
1. **崗位描述 API**:
- ✅ GET `/api/position-descriptions` - 獲取所有崗位描述
- ✅ GET `/api/position-descriptions/<position_code>` - 獲取單一崗位描述
- ✅ POST `/api/position-descriptions` - 新增或更新崗位描述
- ✅ PUT `/api/position-descriptions/<position_code>` - 更新崗位描述
- ✅ DELETE `/api/position-descriptions/<position_code>` - 刪除崗位描述
2. **崗位清單 API**:
- ✅ GET `/api/position-list` - 獲取崗位清單(結合基礎資料與描述)
- ✅ GET `/api/position-list/export` - 匯出完整崗位資料為 CSV
- 支援分頁和搜尋
- 自動合併崗位基礎資料與描述資料
**前端功能實作** (index.html):
1. **崗位描述保存**:
- ✅ 更新 `saveJobDescAndExit()` - 保存後切換到崗位清單
- ✅ 更新 `saveJobDescAndNew()` - 保存後清空表單
- 驗證必填欄位
- 顯示成功/失敗訊息
2. **崗位清單顯示**:
- ✅ 實作 `loadPositionList()` - 從 API 載入資料
- ✅ 實作 `renderPositionList()` - 渲染表格
- ✅ 實作 `viewPositionDesc()` - 檢視崗位描述
- ✅ 實作 `switchModule()` - 模組切換函數
- ✅ 更新表頭欄位(移除事業體/部門,新增崗位性質/等級)
- 自動刷新:切換到崗位清單時自動載入資料
3. **管理者頁面擴充**:
- ✅ 新增「崗位資料管理」區塊
- ✅ 實作 `exportCompletePositionData()` - 匯出完整資料
- ✅ 實作 `refreshPositionStats()` - 更新統計資料
- ✅ 顯示即時統計(總數、已描述、未描述)
- 切換到管理者頁面時自動更新統計
**資料結構**:
```typescript
interface PositionDescription {
id: string;
positionCode: string;
positionName: string;
effectiveDate: string;
jobDuties: string;
requiredSkills: string;
workEnvironment: string;
careerPath: string;
createdAt: string;
updatedAt: string;
}
interface PositionListItem {
positionCode: string;
positionName: string;
positionCategory: string;
positionNature: string;
headcount: string;
positionLevel: string;
effectiveDate: string;
hasDescription: boolean;
jobDuties: string;
requiredSkills: string;
workEnvironment: string;
createdAt: string;
updatedAt: string;
}
```
---
#### 18. 更新文件並推送 Gitea
```
指令: "更新SDD文件 and readme. push to gitea"
指令: "更新所有相關文件後push to gitea"
指令: "更新@/d:/00001_Vibe_coding/1204剛為/USER_COMMANDS_LOG.md , 但排除這個檔案上傳gitea"
```
**執行結果**: ⏳ 進行中
- ✅ 更新 SDD.md 至版本 2.1
- ✅ 更新 README.md 至版本 2.1
- ✅ 更新 USER_COMMANDS_LOG.md本文件
- ⏳ 準備推送至 Gitea排除 USER_COMMANDS_LOG.md
---
## 📊 指令統計
**總計**: 18 個指令
**已完成**: 17 個
**進行中**: 1 個(推送到 Gitea
---
## 🔧 技術決策記錄
### 1. CORS 問題解決方案
- **決策**: 使用 Flask 後端作為代理,避免前端直接呼叫外部 API
- **原因**: 瀏覽器 CORS 政策限制跨域請求
- **實作**: 建立 /api/llm/generate endpoint
### 2. Gemini 模型版本
- **決策**: 使用 gemini-2.5-flash
- **原因**: 使用者明確要求並願意承擔責任
- **風險**: 該模型可能尚未正式發布
### 3. 錯誤處理方式
- **決策**: 建立可關閉、可複製的錯誤對話框
- **原因**: 使用者需要完整查看和複製錯誤訊息
- **實作**: showCopyableError() 函數
### 4. CSV 功能實作
- **決策**: 建立獨立的 csv_utils.js 模組
- **原因**: 模組化設計,可重複使用於多個頁籤
- **優點**: 維護容易,功能統一
---
## 🎯 下一步行動計畫
### 優先順序 1: 完成 CSV 整合
- [ ] 在崗位資料頁籤加入 CSV 按鈕
- [ ] 在職務資料頁籤加入 CSV 按鈕
- [ ] 在崗位描述頁籤加入 CSV 按鈕
- [ ] 測試 CSV 匯入匯出功能
### 優先順序 2: 建立崗位清單頁籤
- [ ] 設計頁籤 UI
- [ ] 實作欄位排序功能
- [ ] 測試排序功能
### 優先順序 3: 建立管理者頁面
- [ ] 設計資料庫 schemausers 表)
- [ ] 建立後端 API/api/users
- [ ] 建立前端管理介面
- [ ] 實作 CRUD 功能
- [ ] 加入權限控制
### 優先順序 4: 全面測試
- [ ] 執行所有功能測試
- [ ] 修正發現的問題
- [ ] 更新文件
### 優先順序 5: 版本控制
- [ ] Commit 新功能
- [ ] 更新 SDD 到 v3.0
- [ ] Push to Gitea
---
## 📝 備註
### 系統環境
- **作業系統**: Windows
- **Python 版本**: 3.x
- **資料庫**: MySQL (mysql.theaken.com:33306)
- **Git 服務**: Gitea (https://gitea.theaken.com/)
- **Flask 端口**: 5000
### 已知問題
1. ✅ CORS 錯誤 - 已修正
2. ✅ Windows 編碼錯誤 - 已修正
3. ✅ 錯誤對話框無法關閉 - 已修正
4. ⚠️ Gemini API Referrer 限制 - 需要使用者自行設定 API Key
### 重要文件清單
1. `.env` - 環境變數設定
2. `SDD.md` - 系統設計文件v2.0
3. `llm_config.py` - LLM API 設定gemini-2.5-flash
4. `start_server.py` - Flask 伺服器啟動腳本
5. `csv_utils.js` - CSV 工具模組
6. `error_handler.js` - 錯誤處理模組
7. `api_test.html` - API 測試頁面
8. `SETUP.md` - 安裝指南
9. `CORS_FIX_GUIDE.md` - CORS 修正指南
10. `GEMINI_API_FIX.md` - Gemini API 修正指南
11. `USER_COMMANDS_LOG.md` - 本文件
---
**文件建立時間**: 2024-12-04
**最後更新**: 2024-12-04
**維護者**: Claude Code
**專案狀態**: 🚧 開發中

388
app.py
View File

@@ -27,11 +27,23 @@ except ImportError:
print("Warning: llm_config not found. LLM features will be disabled.") print("Warning: llm_config not found. LLM features will be disabled.")
LLM_ENABLED = False LLM_ENABLED = False
# Import hierarchy data
try:
from import_hierarchy_data import import_to_memory
hierarchy_data = import_to_memory()
HIERARCHY_ENABLED = True
print("Hierarchy data loaded successfully.")
except Exception as e:
print(f"Warning: Could not load hierarchy data: {e}")
hierarchy_data = None
HIERARCHY_ENABLED = False
app = Flask(__name__, static_folder='.') app = Flask(__name__, static_folder='.')
app.config['JSON_AS_ASCII'] = False # 確保 JSON 正確處理中文
CORS(app) CORS(app)
# 模擬資料庫 (實際應用中應使用 MySQL/PostgreSQL) # 模擬資料庫 (實際應用中應使用 MySQL/PostgreSQL)
positions_db = {} HR_position_positions_db = {}
# 預設崗位資料 # 預設崗位資料
default_positions = { default_positions = {
@@ -75,10 +87,10 @@ default_positions = {
} }
} }
positions_db.update(default_positions) HR_position_positions_db.update(default_positions)
# 職務資料庫 # 職務資料庫
jobs_db = {} HR_position_jobs_db = {}
# 預設職務資料 # 預設職務資料
default_jobs = { default_jobs = {
@@ -101,10 +113,10 @@ default_jobs = {
} }
} }
jobs_db.update(default_jobs) HR_position_jobs_db.update(default_jobs)
# 崗位描述資料庫 # 崗位描述資料庫
position_descriptions_db = {} HR_position_descriptions_db = {}
# 預設崗位描述資料 # 預設崗位描述資料
default_descriptions = { default_descriptions = {
@@ -122,23 +134,51 @@ default_descriptions = {
} }
} }
position_descriptions_db.update(default_descriptions) HR_position_descriptions_db.update(default_descriptions)
# ==================== 靜態頁面 ==================== # ==================== 靜態頁面 ====================
@app.route('/') @app.route('/')
def index(): def index():
"""返回頁面""" """返回登入頁面"""
return send_from_directory('.', 'login.html')
@app.route('/index.html')
def main_app():
"""返回主應用頁面"""
return send_from_directory('.', 'index.html') return send_from_directory('.', 'index.html')
@app.route('/login.html')
def login_page():
"""返回登入頁面"""
return send_from_directory('.', 'login.html')
@app.route('/api-test') @app.route('/api-test')
def api_test_page(): def api_test_page():
"""返回 API 測試頁面""" """返回 API 測試頁面"""
return send_from_directory('.', 'api_test.html') return send_from_directory('.', 'api_test.html')
@app.route('/<path:filename>')
def serve_static(filename):
"""服務靜態文件 (JS, SVG, CSS, etc.)"""
# 只服務特定類型的文件,避免安全問題
allowed_extensions = {'.js', '.svg', '.css', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.md'}
file_ext = os.path.splitext(filename)[1].lower()
if file_ext in allowed_extensions or filename.endswith('.html'):
try:
return send_from_directory('.', filename)
except:
return "File not found", 404
else:
return "File type not allowed", 403
# ==================== 崗位資料 API ==================== # ==================== 崗位資料 API ====================
# CSV 路由必須在 <position_id> 路由之前定義 # CSV 路由必須在 <position_id> 路由之前定義
@@ -238,7 +278,7 @@ def import_positions_csv():
continue continue
# 檢查是否已存在 # 檢查是否已存在
if position_code in positions_db: if position_code in HR_position_positions_db:
errors.append(f"{row_num} 列: 崗位編號 {position_code} 已存在") errors.append(f"{row_num} 列: 崗位編號 {position_code} 已存在")
error_count += 1 error_count += 1
continue continue
@@ -284,7 +324,7 @@ def import_positions_csv():
'updatedAt': now 'updatedAt': now
} }
positions_db[position_code] = new_position HR_position_positions_db[position_code] = new_position
success_count += 1 success_count += 1
except Exception as e: except Exception as e:
@@ -320,7 +360,7 @@ def get_positions():
search = request.args.get('search', '', type=str) search = request.args.get('search', '', type=str)
# 過濾搜尋 # 過濾搜尋
filtered = list(positions_db.values()) filtered = list(HR_position_positions_db.values())
if search: if search:
filtered = [ filtered = [
p for p in filtered p for p in filtered
@@ -349,7 +389,7 @@ def get_positions():
@app.route('/api/positions/<position_id>', methods=['GET']) @app.route('/api/positions/<position_id>', methods=['GET'])
def get_position(position_id): def get_position(position_id):
"""獲取單一崗位資料""" """獲取單一崗位資料"""
if position_id not in positions_db: if position_id not in HR_position_positions_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': '找不到該崗位資料' 'error': '找不到該崗位資料'
@@ -357,7 +397,7 @@ def get_position(position_id):
return jsonify({ return jsonify({
'success': True, 'success': True,
'data': positions_db[position_id] 'data': HR_position_positions_db[position_id]
}) })
@@ -396,7 +436,7 @@ def create_position():
position_code = basic_info['positionCode'] position_code = basic_info['positionCode']
# 檢查是否已存在 # 檢查是否已存在
if position_code in positions_db: if position_code in HR_position_positions_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'崗位編號 {position_code} 已存在' 'error': f'崗位編號 {position_code} 已存在'
@@ -412,7 +452,7 @@ def create_position():
'updatedAt': now 'updatedAt': now
} }
positions_db[position_code] = new_position HR_position_positions_db[position_code] = new_position
return jsonify({ return jsonify({
'success': True, 'success': True,
@@ -436,7 +476,7 @@ def update_position(position_id):
} }
""" """
try: try:
if position_id not in positions_db: if position_id not in HR_position_positions_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': '找不到該崗位資料' 'error': '找不到該崗位資料'
@@ -450,7 +490,7 @@ def update_position(position_id):
}), 400 }), 400
# 更新資料 # 更新資料
existing = positions_db[position_id] existing = HR_position_positions_db[position_id]
if 'basicInfo' in data: if 'basicInfo' in data:
existing['basicInfo'].update(data['basicInfo']) existing['basicInfo'].update(data['basicInfo'])
@@ -476,13 +516,13 @@ def update_position(position_id):
def delete_position(position_id): def delete_position(position_id):
"""刪除崗位資料""" """刪除崗位資料"""
try: try:
if position_id not in positions_db: if position_id not in HR_position_positions_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': '找不到該崗位資料' 'error': '找不到該崗位資料'
}), 404 }), 404
deleted = positions_db.pop(position_id) deleted = HR_position_positions_db.pop(position_id)
return jsonify({ return jsonify({
'success': True, 'success': True,
@@ -503,7 +543,7 @@ def change_position_code(position_id):
Request body: { newCode: "新編號" } Request body: { newCode: "新編號" }
""" """
try: try:
if position_id not in positions_db: if position_id not in HR_position_positions_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': '找不到該崗位資料' 'error': '找不到該崗位資料'
@@ -518,19 +558,19 @@ def change_position_code(position_id):
'error': '請提供新的崗位編號' 'error': '請提供新的崗位編號'
}), 400 }), 400
if new_code in positions_db: if new_code in HR_position_positions_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'崗位編號 {new_code} 已存在' 'error': f'崗位編號 {new_code} 已存在'
}), 409 }), 409
# 更新編號 # 更新編號
position = positions_db.pop(position_id) position = HR_position_positions_db.pop(position_id)
position['id'] = new_code position['id'] = new_code
position['basicInfo']['positionCode'] = new_code position['basicInfo']['positionCode'] = new_code
position['updatedAt'] = datetime.now().isoformat() position['updatedAt'] = datetime.now().isoformat()
positions_db[new_code] = position HR_position_positions_db[new_code] = position
return jsonify({ return jsonify({
'success': True, 'success': True,
@@ -633,7 +673,7 @@ def import_jobs_csv():
continue continue
# 檢查是否已存在 # 檢查是否已存在
if job_code in jobs_db: if job_code in HR_position_jobs_db:
errors.append(f"{row_num} 列: 職務編號 {job_code} 已存在") errors.append(f"{row_num} 列: 職務編號 {job_code} 已存在")
error_count += 1 error_count += 1
continue continue
@@ -658,7 +698,7 @@ def import_jobs_csv():
'updatedAt': now 'updatedAt': now
} }
jobs_db[job_code] = new_job HR_position_jobs_db[job_code] = new_job
success_count += 1 success_count += 1
except Exception as e: except Exception as e:
@@ -696,7 +736,7 @@ def get_jobs():
category = request.args.get('category', '', type=str) category = request.args.get('category', '', type=str)
# 過濾搜尋 # 過濾搜尋
filtered = list(jobs_db.values()) filtered = list(HR_position_jobs_db.values())
if search: if search:
filtered = [ filtered = [
j for j in filtered j for j in filtered
@@ -730,7 +770,7 @@ def get_jobs():
@app.route('/api/jobs/<job_id>', methods=['GET']) @app.route('/api/jobs/<job_id>', methods=['GET'])
def get_job(job_id): def get_job(job_id):
"""獲取單一職務資料""" """獲取單一職務資料"""
if job_id not in jobs_db: if job_id not in HR_position_jobs_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': '找不到該職務資料' 'error': '找不到該職務資料'
@@ -738,7 +778,7 @@ def get_job(job_id):
return jsonify({ return jsonify({
'success': True, 'success': True,
'data': jobs_db[job_id] 'data': HR_position_jobs_db[job_id]
}) })
@@ -785,7 +825,7 @@ def create_job():
job_code = data['jobCode'] job_code = data['jobCode']
# 檢查是否已存在 # 檢查是否已存在
if job_code in jobs_db: if job_code in HR_position_jobs_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'職務編號 {job_code} 已存在' 'error': f'職務編號 {job_code} 已存在'
@@ -800,7 +840,7 @@ def create_job():
'updatedAt': now 'updatedAt': now
} }
jobs_db[job_code] = new_job HR_position_jobs_db[job_code] = new_job
return jsonify({ return jsonify({
'success': True, 'success': True,
@@ -818,7 +858,7 @@ def create_job():
def update_job(job_id): def update_job(job_id):
"""更新職務資料""" """更新職務資料"""
try: try:
if job_id not in jobs_db: if job_id not in HR_position_jobs_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': '找不到該職務資料' 'error': '找不到該職務資料'
@@ -832,7 +872,7 @@ def update_job(job_id):
}), 400 }), 400
# 更新資料 # 更新資料
existing = jobs_db[job_id] existing = HR_position_jobs_db[job_id]
existing.update(data) existing.update(data)
existing['updatedAt'] = datetime.now().isoformat() existing['updatedAt'] = datetime.now().isoformat()
@@ -852,13 +892,13 @@ def update_job(job_id):
def delete_job(job_id): def delete_job(job_id):
"""刪除職務資料""" """刪除職務資料"""
try: try:
if job_id not in jobs_db: if job_id not in HR_position_jobs_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': '找不到該職務資料' 'error': '找不到該職務資料'
}), 404 }), 404
deleted = jobs_db.pop(job_id) deleted = HR_position_jobs_db.pop(job_id)
return jsonify({ return jsonify({
'success': True, 'success': True,
@@ -879,7 +919,7 @@ def change_job_code(job_id):
Request body: { newCode: "新編號" } Request body: { newCode: "新編號" }
""" """
try: try:
if job_id not in jobs_db: if job_id not in HR_position_jobs_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': '找不到該職務資料' 'error': '找不到該職務資料'
@@ -894,19 +934,19 @@ def change_job_code(job_id):
'error': '請提供新的職務編號' 'error': '請提供新的職務編號'
}), 400 }), 400
if new_code in jobs_db: if new_code in HR_position_jobs_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'職務編號 {new_code} 已存在' 'error': f'職務編號 {new_code} 已存在'
}), 409 }), 409
# 更新編號 # 更新編號
job = jobs_db.pop(job_id) job = HR_position_jobs_db.pop(job_id)
job['id'] = new_code job['id'] = new_code
job['jobCode'] = new_code job['jobCode'] = new_code
job['updatedAt'] = datetime.now().isoformat() job['updatedAt'] = datetime.now().isoformat()
jobs_db[new_code] = job HR_position_jobs_db[new_code] = job
return jsonify({ return jsonify({
'success': True, 'success': True,
@@ -1011,14 +1051,14 @@ def get_position_descriptions():
"""獲取所有崗位描述""" """獲取所有崗位描述"""
return jsonify({ return jsonify({
'success': True, 'success': True,
'data': list(position_descriptions_db.values()) 'data': list(HR_position_descriptions_db.values())
}) })
@app.route('/api/position-descriptions/<position_code>', methods=['GET']) @app.route('/api/position-descriptions/<position_code>', methods=['GET'])
def get_position_description(position_code): def get_position_description(position_code):
"""獲取單一崗位描述""" """獲取單一崗位描述"""
if position_code not in position_descriptions_db: if position_code not in HR_position_descriptions_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': '找不到該崗位描述' 'error': '找不到該崗位描述'
@@ -1026,7 +1066,7 @@ def get_position_description(position_code):
return jsonify({ return jsonify({
'success': True, 'success': True,
'data': position_descriptions_db[position_code] 'data': HR_position_descriptions_db[position_code]
}) })
@@ -1034,7 +1074,15 @@ def get_position_description(position_code):
def create_position_description(): def create_position_description():
"""新增或更新崗位描述""" """新增或更新崗位描述"""
try: try:
# 直接從 request.data 讀取並使用 UTF-8 解碼
try:
if request.data:
data = json.loads(request.data.decode('utf-8'))
else:
data = request.get_json() data = request.get_json()
except UnicodeDecodeError:
# 如果 UTF-8 解碼失敗,嘗試其他編碼
data = json.loads(request.data.decode('utf-8', errors='replace'))
if not data: if not data:
return jsonify({ return jsonify({
@@ -1049,24 +1097,24 @@ def create_position_description():
'error': '崗位編號為必填欄位' 'error': '崗位編號為必填欄位'
}), 400 }), 400
# 檢查崗位是否存在 # 檢查崗位是否存在(暫時註解,允許直接新增描述)
if position_code not in positions_db: # if position_code not in HR_position_positions_db:
return jsonify({ # return jsonify({
'success': False, # 'success': False,
'error': f'崗位編號 {position_code} 不存在,請先建立崗位基礎資料' # 'error': f'崗位編號 {position_code} 不存在,請先建立崗位基礎資料'
}), 404 # }), 404
now = datetime.now().isoformat() now = datetime.now().isoformat()
# 如果已存在則更新,否則新增 # 如果已存在則更新,否則新增
if position_code in position_descriptions_db: if position_code in HR_position_descriptions_db:
position_descriptions_db[position_code].update({ HR_position_descriptions_db[position_code].update({
**data, **data,
'updatedAt': now 'updatedAt': now
}) })
message = '崗位描述更新成功' message = '崗位描述更新成功'
else: else:
position_descriptions_db[position_code] = { HR_position_descriptions_db[position_code] = {
'id': position_code, 'id': position_code,
**data, **data,
'createdAt': now, 'createdAt': now,
@@ -1077,7 +1125,7 @@ def create_position_description():
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': message, 'message': message,
'data': position_descriptions_db[position_code] 'data': HR_position_descriptions_db[position_code]
}) })
except Exception as e: except Exception as e:
@@ -1091,7 +1139,7 @@ def create_position_description():
def update_position_description(position_code): def update_position_description(position_code):
"""更新崗位描述""" """更新崗位描述"""
try: try:
if position_code not in position_descriptions_db: if position_code not in HR_position_descriptions_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': '找不到該崗位描述' 'error': '找不到該崗位描述'
@@ -1104,7 +1152,7 @@ def update_position_description(position_code):
'error': '請提供有效的 JSON 資料' 'error': '請提供有效的 JSON 資料'
}), 400 }), 400
position_descriptions_db[position_code].update({ HR_position_descriptions_db[position_code].update({
**data, **data,
'updatedAt': datetime.now().isoformat() 'updatedAt': datetime.now().isoformat()
}) })
@@ -1112,7 +1160,7 @@ def update_position_description(position_code):
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '崗位描述更新成功', 'message': '崗位描述更新成功',
'data': position_descriptions_db[position_code] 'data': HR_position_descriptions_db[position_code]
}) })
except Exception as e: except Exception as e:
@@ -1126,13 +1174,13 @@ def update_position_description(position_code):
def delete_position_description(position_code): def delete_position_description(position_code):
"""刪除崗位描述""" """刪除崗位描述"""
try: try:
if position_code not in position_descriptions_db: if position_code not in HR_position_descriptions_db:
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': '找不到該崗位描述' 'error': '找不到該崗位描述'
}), 404 }), 404
deleted = position_descriptions_db.pop(position_code) deleted = HR_position_descriptions_db.pop(position_code)
return jsonify({ return jsonify({
'success': True, 'success': True,
@@ -1164,8 +1212,8 @@ def get_position_list():
# 組合崗位資料和描述 # 組合崗位資料和描述
combined_list = [] combined_list = []
for position_code, position in positions_db.items(): for position_code, position in HR_position_positions_db.items():
description = position_descriptions_db.get(position_code, {}) description = HR_position_descriptions_db.get(position_code, {})
combined = { combined = {
'positionCode': position_code, 'positionCode': position_code,
@@ -1177,7 +1225,7 @@ def get_position_list():
'effectiveDate': position['basicInfo'].get('effectiveDate', ''), 'effectiveDate': position['basicInfo'].get('effectiveDate', ''),
'minEducation': position['recruitInfo'].get('minEducation', ''), 'minEducation': position['recruitInfo'].get('minEducation', ''),
'salaryRange': position['recruitInfo'].get('salaryRange', ''), 'salaryRange': position['recruitInfo'].get('salaryRange', ''),
'hasDescription': position_code in position_descriptions_db, 'hasDescription': position_code in HR_position_descriptions_db,
'jobDuties': description.get('jobDuties', ''), 'jobDuties': description.get('jobDuties', ''),
'requiredSkills': description.get('requiredSkills', ''), 'requiredSkills': description.get('requiredSkills', ''),
'workEnvironment': description.get('workEnvironment', ''), 'workEnvironment': description.get('workEnvironment', ''),
@@ -1225,8 +1273,8 @@ def export_position_list():
# 組合所有崗位資料 # 組合所有崗位資料
rows = [] rows = []
for position_code, position in positions_db.items(): for position_code, position in HR_position_positions_db.items():
description = position_descriptions_db.get(position_code, {}) description = HR_position_descriptions_db.get(position_code, {})
row = [ row = [
position_code, position_code,
@@ -1372,7 +1420,7 @@ def generate_llm_text():
""" """
使用 LLM API 生成文字 使用 LLM API 生成文字
Request body: { Request body: {
"api": "gemini" | "deepseek" | "openai", "api": "gemini" | "deepseek" | "openai" | "ollama",
"prompt": "提示詞", "prompt": "提示詞",
"max_tokens": 2000 "max_tokens": 2000
} }
@@ -1392,7 +1440,7 @@ def generate_llm_text():
'error': '請提供有效的 JSON 資料' 'error': '請提供有效的 JSON 資料'
}), 400 }), 400
api_name = data.get('api', 'gemini') api_name = data.get('api', 'ollama') # 預設使用 Ollama
prompt = data.get('prompt', '') prompt = data.get('prompt', '')
max_tokens = data.get('max_tokens', 2000) max_tokens = data.get('max_tokens', 2000)
@@ -1410,6 +1458,12 @@ def generate_llm_text():
elif api_name == 'openai': elif api_name == 'openai':
model = data.get('model', 'gpt-3.5-turbo') model = data.get('model', 'gpt-3.5-turbo')
success, result = llm_config.generate_text_openai(prompt, model, max_tokens) success, result = llm_config.generate_text_openai(prompt, model, max_tokens)
elif api_name == 'ollama':
model = data.get('model') # 從請求中獲取模型,如果沒有則使用預設值
success, result = llm_config.generate_text_ollama(prompt, max_tokens, model)
elif api_name == 'gptoss':
model = data.get('model') # 從請求中獲取模型,如果沒有則使用預設值
success, result = llm_config.generate_text_gptoss(prompt, max_tokens, model)
else: else:
return jsonify({ return jsonify({
'success': False, 'success': False,
@@ -1434,6 +1488,198 @@ def generate_llm_text():
}), 500 }), 500
# ==================== 組織階層 API ====================
@app.route('/api/hierarchy/business-units', methods=['GET'])
def get_business_units():
"""獲取所有事業體"""
if not HIERARCHY_ENABLED:
return jsonify({
'success': False,
'error': '組織階層功能未啟用'
}), 503
business_list = [
{'id': v['id'], 'code': v['code'], 'name': v['name']}
for v in hierarchy_data['business_units'].values()
]
business_list.sort(key=lambda x: x['id'])
return jsonify({
'success': True,
'data': business_list
})
@app.route('/api/hierarchy/divisions', methods=['GET'])
def get_divisions():
"""獲取處級單位,可按事業體篩選"""
if not HIERARCHY_ENABLED:
return jsonify({
'success': False,
'error': '組織階層功能未啟用'
}), 503
business_name = request.args.get('business', '', type=str)
if business_name:
# 根據事業體篩選處級單位
division_names = hierarchy_data['businessToDivision'].get(business_name, [])
division_list = [
hierarchy_data['divisions'][name]
for name in division_names
if name in hierarchy_data['divisions']
]
else:
division_list = list(hierarchy_data['divisions'].values())
division_list.sort(key=lambda x: x['id'])
return jsonify({
'success': True,
'data': division_list
})
@app.route('/api/hierarchy/departments', methods=['GET'])
def get_departments():
"""獲取部級單位,可按處級單位篩選"""
if not HIERARCHY_ENABLED:
return jsonify({
'success': False,
'error': '組織階層功能未啟用'
}), 503
division_name = request.args.get('division', '', type=str)
if division_name:
# 根據處級單位篩選部級單位
dept_names = hierarchy_data['divisionToDepartment'].get(division_name, [])
dept_list = [
hierarchy_data['departments'][name]
for name in dept_names
if name in hierarchy_data['departments']
]
else:
dept_list = list(hierarchy_data['departments'].values())
dept_list.sort(key=lambda x: x['id'])
return jsonify({
'success': True,
'data': dept_list
})
@app.route('/api/hierarchy/positions', methods=['GET'])
def get_hierarchy_positions():
"""獲取崗位名稱,可按部級單位篩選"""
if not HIERARCHY_ENABLED:
return jsonify({
'success': False,
'error': '組織階層功能未啟用'
}), 503
department_name = request.args.get('department', '', type=str)
if department_name:
# 根據部級單位篩選崗位
positions = hierarchy_data['departmentToPosition'].get(department_name, [])
else:
# 返回所有不重複的崗位名稱
all_positions = set()
for pos_list in hierarchy_data['departmentToPosition'].values():
all_positions.update(pos_list)
positions = sorted(list(all_positions))
return jsonify({
'success': True,
'data': positions
})
@app.route('/api/hierarchy/full', methods=['GET'])
def get_full_hierarchy():
"""獲取完整階層資料"""
if not HIERARCHY_ENABLED:
return jsonify({
'success': False,
'error': '組織階層功能未啟用'
}), 503
page = request.args.get('page', 1, type=int)
size = request.args.get('size', 50, type=int)
business = request.args.get('business', '', type=str)
division = request.args.get('division', '', type=str)
department = request.args.get('department', '', type=str)
# 篩選資料
filtered = hierarchy_data['organization_positions']
if business:
filtered = [r for r in filtered if r['business'] == business]
if division:
filtered = [r for r in filtered if r['division'] == division]
if department:
filtered = [r for r in filtered if r['department'] == department]
# 分頁
total = len(filtered)
start = (page - 1) * size
end = start + size
paginated = filtered[start:end]
return jsonify({
'success': True,
'data': paginated,
'pagination': {
'page': page,
'size': size,
'total': total,
'totalPages': (total + size - 1) // size
}
})
@app.route('/api/hierarchy/cascade', methods=['GET'])
def get_cascade_data():
"""獲取級聯選擇資料"""
if not HIERARCHY_ENABLED:
return jsonify({
'success': False,
'error': '組織階層功能未啟用'
}), 503
return jsonify({
'success': True,
'data': {
'businessToDivision': hierarchy_data['businessToDivision'],
'divisionToDepartment': hierarchy_data['divisionToDepartment'],
'departmentToPosition': hierarchy_data['departmentToPosition']
}
})
@app.route('/api/hierarchy/stats', methods=['GET'])
def get_hierarchy_stats():
"""獲取組織階層統計"""
if not HIERARCHY_ENABLED:
return jsonify({
'success': False,
'error': '組織階層功能未啟用'
}), 503
return jsonify({
'success': True,
'data': {
'businessUnitsCount': len(hierarchy_data['business_units']),
'divisionsCount': len(hierarchy_data['divisions']),
'departmentsCount': len(hierarchy_data['departments']),
'organizationPositionsCount': len(hierarchy_data['organization_positions'])
}
})
# ==================== 錯誤處理 ==================== # ==================== 錯誤處理 ====================
@app.errorhandler(404) @app.errorhandler(404)
@@ -1486,6 +1732,15 @@ if __name__ == '__main__':
║ GET /api/llm/test-all - 測試所有 API ║ ║ GET /api/llm/test-all - 測試所有 API ║
║ POST /api/llm/generate - 生成文字 ║ ║ POST /api/llm/generate - 生成文字 ║
║ ║ ║ ║
║ 組織階層 API: ║
║ GET /api/hierarchy/business-units - 獲取事業體 ║
║ GET /api/hierarchy/divisions - 獲取處級單位 ║
║ GET /api/hierarchy/departments - 獲取部級單位 ║
║ GET /api/hierarchy/positions - 獲取崗位名稱 ║
║ GET /api/hierarchy/full - 完整階層資料 ║
║ GET /api/hierarchy/cascade - 級聯選擇資料 ║
║ GET /api/hierarchy/stats - 組織統計 ║
║ ║
║ 按 Ctrl+C 停止伺服器 ║ ║ 按 Ctrl+C 停止伺服器 ║
╚══════════════════════════════════════════════════════════════╝ ╚══════════════════════════════════════════════════════════════╝
""") """)
@@ -1500,5 +1755,14 @@ if __name__ == '__main__':
else: else:
print("[!] LLM 功能未啟用 (llm_config.py 未找到)") print("[!] LLM 功能未啟用 (llm_config.py 未找到)")
if HIERARCHY_ENABLED:
print("[OK] 組織階層功能已啟用")
print(f" - 事業體: {len(hierarchy_data['business_units'])}")
print(f" - 處級單位: {len(hierarchy_data['divisions'])}")
print(f" - 部級單位: {len(hierarchy_data['departments'])}")
print(f" - 組織崗位關聯: {len(hierarchy_data['organization_positions'])}")
else:
print("[!] 組織階層功能未啟用")
print() print()
app.run(host='0.0.0.0', port=5000, debug=True) app.run(host='0.0.0.0', port=5000, debug=True)

83
clear_database.sql Normal file
View File

@@ -0,0 +1,83 @@
-- HR Position System - 清空資料庫腳本
-- 只清空業務資料,保留表結構和參照代碼
-- 執行前請確認備份重要資料
USE hr_position_system;
-- ============================================================
-- 1. 停用外鍵檢查(避免刪除順序問題)
-- ============================================================
SET FOREIGN_KEY_CHECKS = 0;
-- ============================================================
-- 2. 清空業務資料表(保留表結構)
-- ============================================================
-- 清空組織崗位關聯表
TRUNCATE TABLE HR_position_organization_positions;
-- 清空部級單位表
TRUNCATE TABLE HR_position_departments;
-- 清空處級單位表
TRUNCATE TABLE HR_position_divisions;
-- 清空事業體表
TRUNCATE TABLE HR_position_business_units;
-- 清空崗位描述表
TRUNCATE TABLE HR_position_job_descriptions;
-- 清空職務基礎資料表
TRUNCATE TABLE HR_position_jobs;
-- 清空崗位基礎資料表
TRUNCATE TABLE HR_position_positions;
-- 清空審計日誌表
TRUNCATE TABLE HR_position_audit_logs;
-- ============================================================
-- 3. 重新啟用外鍵檢查
-- ============================================================
SET FOREIGN_KEY_CHECKS = 1;
-- ============================================================
-- 4. 確認參照代碼表資料保留(不清空)
-- ============================================================
-- reference_codes 表保留所有參照代碼資料
-- 包含POSITION_CATEGORY, POSITION_NATURE, POSITION_LEVEL,
-- JOB_CATEGORY, EDUCATION, SALARY_RANGE, WORK_LOCATION,
-- EMP_ATTRIBUTE, RECRUIT_POSITION, TITLE_REQ
-- 確認參照代碼數量
SELECT code_type, COUNT(*) as count
FROM HR_position_reference_codes
GROUP BY code_type
ORDER BY code_type;
-- ============================================================
-- 5. 顯示清空結果
-- ============================================================
SELECT 'HR_position_organization_positions' as table_name, COUNT(*) as row_count FROM HR_position_organization_positions
UNION ALL
SELECT 'HR_position_departments', COUNT(*) FROM HR_position_departments
UNION ALL
SELECT 'HR_position_divisions', COUNT(*) FROM HR_position_divisions
UNION ALL
SELECT 'HR_position_business_units', COUNT(*) FROM HR_position_business_units
UNION ALL
SELECT 'HR_position_job_descriptions', COUNT(*) FROM HR_position_job_descriptions
UNION ALL
SELECT 'HR_position_jobs', COUNT(*) FROM HR_position_jobs
UNION ALL
SELECT 'HR_position_positions', COUNT(*) FROM HR_position_positions
UNION ALL
SELECT 'HR_position_audit_logs', COUNT(*) FROM HR_position_audit_logs
UNION ALL
SELECT 'HR_position_reference_codes (保留)', COUNT(*) FROM HR_position_reference_codes;
-- ============================================================
-- 完成訊息
-- ============================================================
SELECT '資料庫清空完成!參照代碼已保留。' as message;

13
data/dropdown_data.js Normal file
View File

@@ -0,0 +1,13 @@
// 自動生成的下拉選單資料
// 事業體
const businessUnits = ['半導體事業群', '汽車事業體', '法務室', '岡山製造事業體', '產品事業體', '晶圓三廠', '集團人資行政事業體', '集團財務事業體', '集團會計事業體', '集團資訊事業體', '新創事業體', '稽核室', '總經理室', '總品質事業體', '營業事業體'];
// 處級單位
const deptLevel1Units = ['半導體事業群', '汽車事業體', '法務室', '生產處', '岡山製造事業體', '封裝工程處', '副總辦公室', '測試工程與研發處', '資材處', '廠務與環安衛管理處', '產品事業體', '先進產品事業處', '成熟產品事業處', '晶圓三廠', '製程工程處', '集團人資行政事業體', '集團財務事業體', '岡山強茂財務處', '集團會計事業體', '岡山會計處', '集團會計處', '集團資訊事業體', '資安行動小組', '資訊一處', '資訊二處', '新創事業體', '中低壓產品研發處', '研發中心', '高壓產品研發處', '稽核室', '總經理室', 'ESG專案辦公室', '專案管理室', '總品質事業體', '營業事業體', '商業開發暨市場應用處', '海外銷售事業處', '全球技術服務處', '全球行銷暨業務支援處', '大中華區銷售事業處'];
// 部級單位
const deptLevel2Units = ['生產部', '生產企劃部', '岡山品質管制部', '製程工程一部', '製程工程二部', '設備一部', '設備二部', '工業工程部', '測試工程部', '新產品導入部', '研發部', '採購部', '外部資源部', '生管部', '原物料控制部', '廠務部', '產品管理部(APD)', '產品管理部(MPD)', '品質部', '製造部', '廠務部(Fab3)', '工程一部', '工程二部', '工程三部', '製程整合部(Fab3)', '行政總務管理部', '招募任用部', '訓練發展部', '薪酬管理部', '岡山強茂財務部', '會計部', '管理會計部', '集團合併報表部', '應用系統部', '電腦整合製造部', '系統網路服務部', '資源管理部', '客戶品質管理部', '產品品質管理部', '品質系統及客戶工程整合部', '封測外包品質管理部', '品質保證部', '日本區暨代工業務部', '歐亞區業務部', '韓國區業務部-韓國區', '美洲區業務部', '應用工程部(GTS)', '系統工程部', '特性測試部', '業務生管部', '市場行銷企劃部', 'MOSFET晶圓採購部', '台灣區業務部', '業務一部', '業務二部'];
// 崗位名稱
const positionNames = ['營運長', '營運長助理', '副總經理', '專案經理', '經副理', '法務專員', '專利工程師', '處長', '專員', '課長', '組長', '班長', '副班長', '作業員', '工程師', '副總經理助理', '副理', '專案經副理', '顧問', '人資長', '助理', '財務長', '專案副理', '會計長', '資訊長', '主任', '總裁', '總經理', '專員/工程師', '經理', '技術經副理', '處長/資深經理'];

2066
data/hierarchical_data.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ USE hr_position_system;
-- ============================================================ -- ============================================================
-- Table: positions (崗位基礎資料) -- Table: positions (崗位基礎資料)
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS positions ( CREATE TABLE IF NOT EXISTS HR_position_positions (
id VARCHAR(20) PRIMARY KEY COMMENT '崗位編號', id VARCHAR(20) PRIMARY KEY COMMENT '崗位編號',
position_code VARCHAR(20) NOT NULL UNIQUE COMMENT '崗位編號', position_code VARCHAR(20) NOT NULL UNIQUE COMMENT '崗位編號',
position_name VARCHAR(100) NOT NULL COMMENT '崗位名稱', position_name VARCHAR(100) NOT NULL COMMENT '崗位名稱',
@@ -59,7 +59,7 @@ CREATE TABLE IF NOT EXISTS positions (
-- ============================================================ -- ============================================================
-- Table: jobs (職務基礎資料) -- Table: jobs (職務基礎資料)
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS jobs ( CREATE TABLE IF NOT EXISTS HR_position_jobs (
id VARCHAR(20) PRIMARY KEY COMMENT '職務編號', id VARCHAR(20) PRIMARY KEY COMMENT '職務編號',
job_category_code VARCHAR(4) NOT NULL COMMENT '職務類別編號', job_category_code VARCHAR(4) NOT NULL COMMENT '職務類別編號',
job_category_name VARCHAR(50) COMMENT '職務類別名稱', job_category_name VARCHAR(50) COMMENT '職務類別名稱',
@@ -88,7 +88,7 @@ CREATE TABLE IF NOT EXISTS jobs (
-- ============================================================ -- ============================================================
-- Table: job_descriptions (崗位描述) -- Table: job_descriptions (崗位描述)
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS job_descriptions ( CREATE TABLE IF NOT EXISTS HR_position_job_descriptions (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵', id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
emp_no VARCHAR(20) COMMENT '工號', emp_no VARCHAR(20) COMMENT '工號',
emp_name VARCHAR(50) COMMENT '姓名', emp_name VARCHAR(50) COMMENT '姓名',
@@ -126,13 +126,13 @@ CREATE TABLE IF NOT EXISTS job_descriptions (
INDEX idx_emp_no (emp_no), INDEX idx_emp_no (emp_no),
INDEX idx_position_code (position_code), INDEX idx_position_code (position_code),
INDEX idx_version_date (version_date), INDEX idx_version_date (version_date),
FOREIGN KEY (position_code) REFERENCES positions(position_code) ON DELETE SET NULL FOREIGN KEY (position_code) REFERENCES HR_position_positions(position_code) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='崗位描述表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='崗位描述表';
-- ============================================================ -- ============================================================
-- Table: reference_codes (參照資料代碼表) -- Table: reference_codes (參照資料代碼表)
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS reference_codes ( CREATE TABLE IF NOT EXISTS HR_position_reference_codes (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵', id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
code_type VARCHAR(50) NOT NULL COMMENT '代碼類型', code_type VARCHAR(50) NOT NULL COMMENT '代碼類型',
code_value VARCHAR(10) NOT NULL COMMENT '代碼值', code_value VARCHAR(10) NOT NULL COMMENT '代碼值',
@@ -152,7 +152,7 @@ CREATE TABLE IF NOT EXISTS reference_codes (
-- ============================================================ -- ============================================================
-- Table: audit_logs (審計日誌) -- Table: audit_logs (審計日誌)
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS audit_logs ( CREATE TABLE IF NOT EXISTS HR_position_audit_logs (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵', id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
action VARCHAR(20) NOT NULL COMMENT '操作類型(CREATE/UPDATE/DELETE)', action VARCHAR(20) NOT NULL COMMENT '操作類型(CREATE/UPDATE/DELETE)',
entity_type VARCHAR(50) NOT NULL COMMENT '實體類型', entity_type VARCHAR(50) NOT NULL COMMENT '實體類型',
@@ -174,21 +174,21 @@ CREATE TABLE IF NOT EXISTS audit_logs (
-- ============================================================ -- ============================================================
-- 崗位類別 (Position Category) -- 崗位類別 (Position Category)
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
('POSITION_CATEGORY', '01', '技術職', 1), ('POSITION_CATEGORY', '01', '技術職', 1),
('POSITION_CATEGORY', '02', '管理職', 2), ('POSITION_CATEGORY', '02', '管理職', 2),
('POSITION_CATEGORY', '03', '業務職', 3), ('POSITION_CATEGORY', '03', '業務職', 3),
('POSITION_CATEGORY', '04', '行政職', 4); ('POSITION_CATEGORY', '04', '行政職', 4);
-- 崗位性質 (Position Nature) -- 崗位性質 (Position Nature)
INSERT INTO reference_codes (code_type, code_value, code_name, code_name_en, sort_order) VALUES INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, code_name_en, sort_order) VALUES
('POSITION_NATURE', 'FT', '全職', 'Full-time', 1), ('POSITION_NATURE', 'FT', '全職', 'Full-time', 1),
('POSITION_NATURE', 'PT', '兼職', 'Part-time', 2), ('POSITION_NATURE', 'PT', '兼職', 'Part-time', 2),
('POSITION_NATURE', 'CT', '約聘', 'Contract', 3), ('POSITION_NATURE', 'CT', '約聘', 'Contract', 3),
('POSITION_NATURE', 'IN', '實習', 'Intern', 4); ('POSITION_NATURE', 'IN', '實習', 'Intern', 4);
-- 崗位級別 (Position Level) -- 崗位級別 (Position Level)
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
('POSITION_LEVEL', 'L1', '基層員工', 1), ('POSITION_LEVEL', 'L1', '基層員工', 1),
('POSITION_LEVEL', 'L2', '資深員工', 2), ('POSITION_LEVEL', 'L2', '資深員工', 2),
('POSITION_LEVEL', 'L3', '主管', 3), ('POSITION_LEVEL', 'L3', '主管', 3),
@@ -198,7 +198,7 @@ INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUE
('POSITION_LEVEL', 'L7', '總經理', 7); ('POSITION_LEVEL', 'L7', '總經理', 7);
-- 職務類別 (Job Category) -- 職務類別 (Job Category)
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
('JOB_CATEGORY', 'MGR', '管理職', 1), ('JOB_CATEGORY', 'MGR', '管理職', 1),
('JOB_CATEGORY', 'TECH', '技術職', 2), ('JOB_CATEGORY', 'TECH', '技術職', 2),
('JOB_CATEGORY', 'SALE', '業務職', 3), ('JOB_CATEGORY', 'SALE', '業務職', 3),
@@ -207,7 +207,7 @@ INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUE
('JOB_CATEGORY', 'PROD', '生產職', 6); ('JOB_CATEGORY', 'PROD', '生產職', 6);
-- 學歷 (Education) -- 學歷 (Education)
INSERT INTO reference_codes (code_type, code_value, code_name, code_name_en, sort_order) VALUES INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, code_name_en, sort_order) VALUES
('EDUCATION', 'HS', '高中', 'High School', 1), ('EDUCATION', 'HS', '高中', 'High School', 1),
('EDUCATION', 'JC', '專科', 'Junior College', 2), ('EDUCATION', 'JC', '專科', 'Junior College', 2),
('EDUCATION', 'BA', '大學', 'Bachelor', 3), ('EDUCATION', 'BA', '大學', 'Bachelor', 3),
@@ -215,7 +215,7 @@ INSERT INTO reference_codes (code_type, code_value, code_name, code_name_en, sor
('EDUCATION', 'PHD', '博士', 'PhD', 5); ('EDUCATION', 'PHD', '博士', 'PhD', 5);
-- 薪酬范圍 (Salary Range) -- 薪酬范圍 (Salary Range)
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
('SALARY_RANGE', 'A', 'A級', 1), ('SALARY_RANGE', 'A', 'A級', 1),
('SALARY_RANGE', 'B', 'B級', 2), ('SALARY_RANGE', 'B', 'B級', 2),
('SALARY_RANGE', 'C', 'C級', 3), ('SALARY_RANGE', 'C', 'C級', 3),
@@ -224,7 +224,7 @@ INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUE
('SALARY_RANGE', 'N', '面議', 6); ('SALARY_RANGE', 'N', '面議', 6);
-- 任職地點 (Work Location) -- 任職地點 (Work Location)
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
('WORK_LOCATION', 'HQ', '總部', 1), ('WORK_LOCATION', 'HQ', '總部', 1),
('WORK_LOCATION', 'TPE', '台北辦公室', 2), ('WORK_LOCATION', 'TPE', '台北辦公室', 2),
('WORK_LOCATION', 'TYC', '桃園廠區', 3), ('WORK_LOCATION', 'TYC', '桃園廠區', 3),
@@ -233,7 +233,7 @@ INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUE
('WORK_LOCATION', 'SZ', '深圳辦公室', 6); ('WORK_LOCATION', 'SZ', '深圳辦公室', 6);
-- 員工屬性 (Employee Attribute) -- 員工屬性 (Employee Attribute)
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
('EMP_ATTRIBUTE', 'FT', '正式員工', 1), ('EMP_ATTRIBUTE', 'FT', '正式員工', 1),
('EMP_ATTRIBUTE', 'CT', '約聘人員', 2), ('EMP_ATTRIBUTE', 'CT', '約聘人員', 2),
('EMP_ATTRIBUTE', 'PT', '兼職人員', 3), ('EMP_ATTRIBUTE', 'PT', '兼職人員', 3),
@@ -241,7 +241,7 @@ INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUE
('EMP_ATTRIBUTE', 'DP', '派遣人員', 5); ('EMP_ATTRIBUTE', 'DP', '派遣人員', 5);
-- 招聘職位 (Recruit Position) -- 招聘職位 (Recruit Position)
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
('RECRUIT_POSITION', 'ENG', '工程師', 1), ('RECRUIT_POSITION', 'ENG', '工程師', 1),
('RECRUIT_POSITION', 'MGR', '經理', 2), ('RECRUIT_POSITION', 'MGR', '經理', 2),
('RECRUIT_POSITION', 'AST', '助理', 3), ('RECRUIT_POSITION', 'AST', '助理', 3),
@@ -249,11 +249,93 @@ INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUE
('RECRUIT_POSITION', 'SAL', '業務', 5); ('RECRUIT_POSITION', 'SAL', '業務', 5);
-- 職稱要求 (Title Requirement) -- 職稱要求 (Title Requirement)
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
('TITLE_REQ', 'NONE', '無要求', 1), ('TITLE_REQ', 'NONE', '無要求', 1),
('TITLE_REQ', 'CERT', '需證書', 2), ('TITLE_REQ', 'CERT', '需證書', 2),
('TITLE_REQ', 'LIC', '需執照', 3); ('TITLE_REQ', 'LIC', '需執照', 3);
-- ============================================================
-- Table: business_units (事業體)
-- ============================================================
CREATE TABLE IF NOT EXISTS HR_position_business_units (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
business_code VARCHAR(20) NOT NULL UNIQUE COMMENT '事業體代碼',
business_name VARCHAR(100) NOT NULL COMMENT '事業體名稱',
sort_order INT DEFAULT 0 COMMENT '排序',
is_active BOOLEAN DEFAULT TRUE COMMENT '是否啟用',
remark VARCHAR(500) COMMENT '備註',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
INDEX idx_business_name (business_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='事業體表';
-- ============================================================
-- Table: divisions (處級單位)
-- ============================================================
CREATE TABLE IF NOT EXISTS HR_position_divisions (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
division_code VARCHAR(20) NOT NULL UNIQUE COMMENT '處級單位代碼',
division_name VARCHAR(100) NOT NULL COMMENT '處級單位名稱',
business_id INT COMMENT '所屬事業體ID',
sort_order INT DEFAULT 0 COMMENT '排序',
is_active BOOLEAN DEFAULT TRUE COMMENT '是否啟用',
remark VARCHAR(500) COMMENT '備註',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
INDEX idx_division_name (division_name),
INDEX idx_business_id (business_id),
FOREIGN KEY (business_id) REFERENCES HR_position_business_units(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='處級單位表';
-- ============================================================
-- Table: departments (部級單位)
-- ============================================================
CREATE TABLE IF NOT EXISTS HR_position_departments (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
department_code VARCHAR(20) NOT NULL UNIQUE COMMENT '部級單位代碼',
department_name VARCHAR(100) NOT NULL COMMENT '部級單位名稱',
division_id INT COMMENT '所屬處級單位ID',
sort_order INT DEFAULT 0 COMMENT '排序',
is_active BOOLEAN DEFAULT TRUE COMMENT '是否啟用',
remark VARCHAR(500) COMMENT '備註',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
INDEX idx_department_name (department_name),
INDEX idx_division_id (division_id),
FOREIGN KEY (division_id) REFERENCES HR_position_divisions(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部級單位表';
-- ============================================================
-- Table: organization_positions (組織崗位關聯)
-- ============================================================
CREATE TABLE IF NOT EXISTS HR_position_organization_positions (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
business_id INT NOT NULL COMMENT '事業體ID',
division_id INT NOT NULL COMMENT '處級單位ID',
department_id INT NOT NULL COMMENT '部級單位ID',
position_title VARCHAR(100) NOT NULL COMMENT '崗位名稱',
sort_order INT DEFAULT 0 COMMENT '排序',
is_active BOOLEAN DEFAULT TRUE COMMENT '是否啟用',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
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),
FOREIGN KEY (business_id) REFERENCES HR_position_business_units(id) ON DELETE CASCADE,
FOREIGN KEY (division_id) REFERENCES HR_position_divisions(id) ON DELETE CASCADE,
FOREIGN KEY (department_id) REFERENCES HR_position_departments(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='組織崗位關聯表';
-- ============================================================ -- ============================================================
-- End of Schema -- End of Schema
-- ============================================================ -- ============================================================

View File

@@ -0,0 +1,19 @@
人工智能AI就像教電腦模仿人類的思考或行為能力。簡單來說它是讓機器能夠
1. **學習**:從大量數據或經驗中自己找出規律(例如:辨識貓的照片)。
2. **判斷**:根據學習到的資訊做出決策(例如:推薦你喜歡的影片)。
3. **解決問題**:處理複雜任務,如下棋、翻譯語言,甚至開車(自動駕駛)。
---
### 生活中的例子:
- **手機語音助手**(如 Siri能聽懂你的問題並回答。
- **社群媒體** 自動標註照片中的人臉。
- **地圖軟體** 根據交通狀況規劃最快路線。
---
### 核心概念:
AI 不是真的擁有「智慧」,而是透過數學模型和大量資料訓練出的「模擬智能」。目前常見的 AI 通常專精於特定任務(例如:只會下圍棋的 AlphaGo還無法像人類一樣全面思考。
AI 正在快速發展,未來可能會更深入影響生活、工作和醫療等領域,但也需要關注相關的倫理與安全問題哦! 😊

View File

@@ -0,0 +1,183 @@
# 系統表單欄位規範書 v1.1 (UX Optimized)
**設計原則 (UX Design Principles):**
1. **Context First**: 先確認組織架構 (BU/Dept),再定義具體內容。
2. **Key Data Top**: 核心識別資料 (編號/名稱) 置頂。
3. **Group by Type**: 下拉選單 (Select) 集中,開關 (Switch) 集中,長文字 (Textarea) 沉底。
---
## 1. 崗位基礎資料模組 (Position Module)
**表單代號**: `positionForm`
**資料表**: `Position`
### 1.1 基礎資料頁籤 (tab-position-basic)
> **UX 邏輯**: 先選組織(過濾條件) → 填寫核心識別 → 設定分類屬性 → 填寫詳細描述。
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|---|---|---|---|---|---|---|
| **[組織定義]** | | | | | | |
| 1 | 事業體 | `pos_businessUnit` | `businessUnit` | select | 否 | 聯動 L1 |
| 2 | 處級單位 | `pos_division` | `division` | select | 否 | 聯動 L2 |
| 3 | 部級單位 | `pos_department` | `department` | select | 否 | 聯動 L3 |
| 4 | 課級單位 | `pos_section` | `section` | text | 否 | - |
| **[核心識別]** | | | | | | |
| 5 | **崗位編號** | `pos_code` | `positionCode` | text | **是** | 唯一識別 |
| 6 | **崗位名稱** | `pos_name` | `positionName` | text | **是** | - |
| 7 | 生效日期 | `pos_effectiveDate` | `effectiveDate` | date | 否 | 預設今日 |
| **[分類屬性]** | | | | | | |
| 8 | 崗位級別 | `pos_level` | `positionLevel` | select | 否 | L1-L7 |
| 9 | 崗位類別 | `pos_category` | `positionCategory` | select | 否 | 連動帶出名稱 |
| 10 | 崗位類別名稱 | `pos_categoryName` | `positionCategoryName` | text | 否 | Readonly |
| 11 | 崗位性質 | `pos_type` | `positionType` | select | 否 | FT/PT/CT... |
| 12 | 崗位性質名稱 | `pos_typeName` | `positionTypeName` | text | 否 | Readonly |
| 13 | 編制人數 | `pos_headcount` | `headcount` | number | 否 | - |
| **[詳細描述]** | | | | | | |
| 14 | 崗位描述 | `pos_desc` | `description` | textarea | 否 | rows=6 |
| 15 | 崗位備注 | `pos_remark` | `remark` | textarea | 否 | rows=4 |
### 1.2 招聘要求資料頁籤 (tab-position-recruit)
> **UX 邏輯**: 先定義「要招什麼樣的人(Target)」→「硬性條件(Must)」→「軟性技能(Plus)」→「廣告文案(Text)」。
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|---|---|---|---|---|---|---|
| **[招聘職位定義]** | | | | | | |
| 1 | 招聘職位代碼 | `rec_position` | `recruitPosition` | select | 否 | ENG, MGR... |
| 2 | 對外職稱 | `rec_jobTitle` | `jobTitle` | text | 否 | 顯示在招聘網的名稱 |
| 3 | 上級崗位編號 | `rec_superiorCode` | `superiorPositionCode` | text | 否 | - |
| 4 | 工作性質 | `rec_jobType` | `jobType` | select | 否 | 招聘用分類 |
| **[硬性資格]** | | | | | | |
| 5 | 最低學歷 | `rec_eduLevel` | `educationLevel` | select | 否 | - |
| 6 | 專業要求 | `rec_majorReq` | `majorRequirements` | text | 否 | Modal 選擇 |
| 7 | 工作經驗 | `rec_expYears` | `experienceYears` | select | 否 | - |
| 8 | 薪酬范圍 | `rec_salaryRange` | `salaryRange` | select | 否 | - |
| 9 | 要求性別 | `rec_gender` | `requiredGender` | select | 否 | - |
| 10 | 年齡限制 (Min) | `rec_minAge` | `minAge` | number | 否 | 併排顯示 |
| 11 | 年齡限制 (Max) | `rec_maxAge` | `maxAge` | number | 否 | 併排顯示 |
| **[技能與證照]** | | | | | | |
| 12 | 語言要求 | `rec_langReq` | `langRequirements` | text | 否 | - |
| 13 | 證照要求 | `rec_certReq` | `certRequirements` | select | 否 | - |
| 14 | 技能要求 | `rec_skillReq` | `skillRequirements` | text | 否 | Tag input |
| 15 | 其他要求 | `rec_otherReq` | `otherRequirements` | text | 否 | - |
| **[文案描述]** | | | | | | |
| 16 | 職位描述 (JD) | `rec_jobDesc` | `recruitJobDesc` | textarea | 否 | 廣告用 |
| 17 | 崗位要求 (Req) | `rec_positionReq` | `recruitRequirements` | textarea | 否 | 廣告用 |
| 18 | 招聘備注 | `rec_remark` | `recruitRemark` | textarea | 否 | 內部用 |
---
## 2. 職務基礎資料模組 (Job Module)
**表單代號**: `jobForm`
**資料表**: `Job`
> **UX 邏輯**: 先分類 → 再命名 → 設定權限/福利(開關) → 備註。
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|---|---|---|---|---|---|---|
| **[分類與識別]** | | | | | | |
| 1 | **職務類別** | `job_category` | `jobCategoryCode` | select | **是** | - |
| 2 | 職務類別名稱 | `job_categoryName` | `jobCategoryName` | text | 否 | Readonly |
| 3 | **職務編號** | `job_code` | `jobCode` | text | **是** | - |
| 4 | **職務名稱** | `job_name` | `jobName` | text | **是** | - |
| 5 | 職務英文名稱 | `job_nameEn` | `jobNameEn` | text | 否 | - |
| **[屬性設定]** | | | | | | |
| 6 | 職務層級 | `job_level` | `jobLevel` | text | 否 | 敏感欄位 |
| 7 | 生效日期 | `job_effectiveDate`| `effectiveDate` | date | 否 | - |
| 8 | 排列順序 | `job_sortOrder` | `sortOrder` | number | 否 | - |
| 9 | 預算編制人數 | `job_headcount` | `headcount` | number | 否 | - |
| **[福利開關]** | | | | | | |
| 10 | 全勤獎金 | `job_hasAttBonus` | `hasAttendanceBonus` | checkbox| 否 | Toggle Switch |
| 11 | 住房補貼 | `job_hasHouseAllow`| `hasHousingAllowance` | checkbox| 否 | Toggle Switch |
| **[備註]** | | | | | | |
| 12 | 職務備注 | `job_remark` | `remark` | textarea | 否 | - |
---
## 3. 部門職責模組 (DeptFunction Module)
**表單代號**: `deptFunctionForm`
**資料表**: `DeptFunction`
> **UX 邏輯**: 組織架構 → 核心職責定義 → 管理與規模 → 策略性描述(使命/願景)。
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|---|---|---|---|---|---|---|
| **[基本定義]** | | | | | | |
| 1 | **事業體** | `df_businessUnit` | `businessUnit` | select | **是** | - |
| 2 | **處級單位** | `df_division` | `division` | select | **是** | - |
| 3 | **部級單位** | `df_department` | `department` | select | **是** | - |
| 4 | 課級單位 | `df_section` | `section` | text | 否 | - |
| 5 | **職責編號** | `df_code` | `dfCode` | text | **是** | - |
| 6 | **職責名稱** | `df_name` | `dfName` | text | **是** | - |
| **[管理架構]** | | | | | | |
| 7 | **對應崗位** | `df_posTitle` | `positionTitle` | select | **是** | 綁定 Position |
| 8 | 崗位級別 | `df_posLevel` | `positionLevel` | select | 否 | - |
| 9 | 部門主管職稱 | `df_managerTitle` | `managerTitle` | text | 否 | - |
| 10 | 人數上限 | `df_headcountLimit`| `headcountLimit` | number | 否 | - |
| 11 | **生效日期** | `df_effectiveDate` | `effectiveDate` | date | **是** | - |
| 12 | 狀態 | `df_status` | `status` | select | 否 | Active/Inactive |
| **[策略職責]** | | | | | | |
| 13 | **核心職責** | `df_coreFunc` | `coreFunctions` | textarea | **是** | 重點欄位 (Top) |
| 14 | 部門使命 | `df_mission` | `mission` | textarea | 否 | - |
| 15 | 部門願景 | `df_vision` | `vision` | textarea | 否 | - |
| 16 | KPIs | `df_kpis` | `kpis` | textarea | 否 | - |
| 17 | 協作部門 | `df_collab` | `collaboration` | textarea | 否 | - |
| 18 | 備注 | `df_remark` | `remark` | textarea | 否 | - |
---
## 4. 崗位描述模組 (JobDescription Module)
**表單代號**: `jobDescForm`
**資料表**: `JobDescription`
> **UX 邏輯**:
> 1. Header: 快速鎖定「這是誰的JD」。
> 2. Readonly Area: 顯示「他在哪裡工作」(提供撰寫JD的上下文)。
> 3. Reporting: 釐清「上下級關係」。
> 4. Main Body: 撰寫「做什麼」(Purpose/Resp)。
> 5. Requirements: 撰寫「需要什麼條件」。
### 4.1 識別與上下文 (Header & Context)
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|---|---|---|---|---|---|---|
| **[員工綁定]** | | | | | | |
| 1 | 選擇員工(工號) | `jd_empNo` | `empNo` | text | 否 | 搜尋 Modal |
| 2 | 姓名 | `jd_empName` | `empName` | text | 否 | Readonly |
| 3 | 版本日期 | `jd_versionDate` | `versionDate` | date | 否 | - |
| **[崗位背景 (唯讀)]** | | | | | | |
| 4 | 崗位代碼 | `jd_posCode` | `positionCode` | text | 否 | 關聯鍵 |
| 5 | 崗位名稱 | `jd_posName` | `positionName` | text | 否 | Readonly |
| 6 | 所屬部門 | `jd_department` | `department` | text | 否 | Readonly (組合字串) |
| 7 | 任職地點 | `jd_location` | `workLocation` | select | 否 | - |
| 8 | 員工屬性 | `jd_empAttr` | `empAttribute` | select | 否 | FT/PT... |
| 9 | 部門職責引用 | `jd_dfCode` | `dfCode` | text | 否 | 關聯 DF 模組 |
### 4.2 匯報關係 (Reporting Lines)
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|---|---|---|---|---|---|---|
| 10 | **直接主管** | `jd_supervisor` | `directSupervisor` | text | 否 | 姓名/職稱 |
| 11 | **匯報對象** | `jd_reportTo` | `reportTo` | text | 否 | 系統職務節點 |
| 12 | 職等&職務 | `jd_gradeJob` | `positionGradeJob` | text | 否 | HR 用級別 |
| 13 | 直接下屬 | `jd_directReports` | `directReports` | text | 否 | 人數或職稱 |
### 4.3 職責詳情 (Responsibilities)
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|---|---|---|---|---|---|---|
| 14 | 崗位設置目的 | `jd_purpose` | `positionPurpose` | text | 否 | 一句話摘要 |
| 15 | **主要職責** | `jd_mainResp` | `mainResponsibilities`| textarea | 否 | 核心內容 (大區塊) |
### 4.4 任職資格 (Requirements)
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|---|---|---|---|---|---|---|
| 16 | 教育程度 | `jd_eduLevel` | `educationLevel` | text | 否 | - |
| 17 | 工作經驗 | `jd_expReq` | `experienceRequirements`| textarea | 否 | - |
| 18 | 專業知識 | `jd_proKnowledge` | `professionalKnowledge` | textarea | 否 | - |
| 19 | 基本技能 | `jd_basicSkills` | `basicSkills` | textarea | 否 | - |
| 20 | 其他要求 | `jd_otherReq` | `otherRequirements` | textarea | 否 | - |

View File

@@ -0,0 +1,286 @@
# HTML ID 重命名對照表
## 變更概覽
- **總計需變更72 個**
- **無需變更13 個**
- **總欄位數85 個**
---
## 模組 1: 崗位基礎資料 - 基礎資料頁籤
**需變更15 / 15**
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|-------|-------|---------|------|
| `businessUnit` | `pos_businessUnit` | 事業體 (Business Unit) | |
| `division` | `pos_division` | 處級單位 (Division) | |
| `department` | `pos_department` | 部級單位 (Department) | |
| `section` | `pos_section` | 課級單位 (Section) | |
| `positionCode` | `pos_code` | 崗位編號 | 必填欄位 |
| `effectiveDate` | `pos_effectiveDate` | 生效日期 | |
| `positionName` | `pos_name` | 崗位名稱 | 必填欄位 |
| `positionLevel` | `pos_level` | 崗位級別 | |
| `positionCategory` | `pos_category` | 崗位類別 | onchange 事件 |
| `positionCategoryName` | `pos_categoryName` | 崗位類別名稱 | readonly |
| `positionNature` | `pos_type` | 崗位性質 | onchange 事件,注意:資料庫改為 positionType |
| `positionNatureName` | `pos_typeName` | 崗位性質名稱 | readonly資料庫改為 positionTypeName |
| `headcount` | `pos_headcount` | 編制人數 | |
| `positionDesc` | `pos_desc` | 崗位描述 | 資料庫改為 description |
| `positionRemark` | `pos_remark` | 崗位備注 | 資料庫改為 remark |
---
## 模組 2: 崗位基礎資料 - 招聘要求頁籤
**需變更18 / 18**
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|-------|-------|---------|------|
| `minEducation` | `rec_eduLevel` | 最低學歷 | 資料庫改為 educationLevel |
| `requiredGender` | `rec_gender` | 要求性別 | |
| `salaryRange` | `rec_salaryRange` | 薪酬范圍 | |
| `workExperience` | `rec_expYears` | 工作經驗 | 資料庫改為 experienceYears |
| `minAge` | `rec_minAge` | 最小年齡 | |
| `maxAge` | `rec_maxAge` | 最大年齡 | |
| `jobType` | `rec_jobType` | 工作性質 | |
| `recruitPosition` | `rec_position` | 招聘職位 | 資料庫改為 recruitPosition |
| `jobTitle` | `rec_jobTitle` | 職位名稱(對外) | |
| `superiorPosition` | `rec_superiorCode` | 上級崗位編號 | 資料庫改為 superiorPositionCode |
| `jobDesc` | `rec_jobDesc` | 職位描述(JD) | 資料庫改為 recruitJobDesc |
| `positionReq` | `rec_positionReq` | 崗位要求(Req) | 資料庫改為 recruitRequirements |
| `titleReq` | `rec_certReq` | 職稱要求 | 語義更正為「證照要求」,資料庫改為 certRequirements |
| `majorReq` | `rec_majorReq` | 專業要求 | 資料庫改為 majorRequirements |
| `skillReq` | `rec_skillReq` | 技能要求 | 資料庫改為 skillRequirements |
| `langReq` | `rec_langReq` | 語言要求 | 資料庫改為 langRequirements |
| `otherReq` | `rec_otherReq` | 其他要求 | 資料庫改為 otherRequirements |
| `recruitRemark` | `rec_remark` | 招聘備注 | 資料庫改為 recruitRemark |
---
## 模組 3: 職務基礎資料
**需變更12 / 12**
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|-------|-------|---------|------|
| `jobCategoryCode` | `job_category` | 職務類別編號 | 必填欄位onchange 事件 |
| `jobCategoryName` | `job_categoryName` | 職務類別名稱 | readonly |
| `jobCode` | `job_code` | 職務編號 | 必填欄位 |
| `jobName` | `job_name` | 職務名稱 | 必填欄位 |
| `jobNameEn` | `job_nameEn` | 職務英文 | |
| `jobEffectiveDate` | `job_effectiveDate` | 生效日期 | 資料庫改為 effectiveDate |
| `jobLevel` | `job_level` | 職務層級 | 敏感欄位 |
| `jobHeadcount` | `job_headcount` | 編制人數 | 資料庫改為 headcount |
| `jobSortOrder` | `job_sortOrder` | 排列順序 | 資料庫改為 sortOrder |
| `hasAttendanceBonus` | `job_hasAttBonus` | 全勤獎金 | Toggle Switch |
| `hasHousingAllowance` | `job_hasHouseAllow` | 住房補貼 | Toggle Switch |
| `jobRemark` | `job_remark` | 職務備注 | 資料庫改為 remark |
---
## 模組 4: 部門職責
**需變更19 / 19**
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|-------|-------|---------|------|
| `deptFunctionCode` | `df_code` | 部門職責編號 | 必填欄位,資料庫改為 dfCode |
| `deptFunctionName` | `df_name` | 部門職責名稱 | 必填欄位,資料庫改為 dfName |
| `deptFunctionBU` | `df_businessUnit` | 事業體 (第1個) | 必填欄位,已合併重複欄位 |
| `deptFunc_businessUnit` | `df_businessUnit` | 事業體 (第2個) | **合併為同一欄位** |
| `deptFunc_division` | `df_division` | 處級單位 | 必填欄位 |
| `deptFunc_department` | `df_department` | 部級單位 | 必填欄位 |
| `deptFunc_section` | `df_section` | 課級單位 | |
| `deptFunc_positionTitle` | `df_posTitle` | 崗位名稱 | 必填欄位,資料庫改為 positionTitle |
| `deptFunc_positionLevel` | `df_posLevel` | 崗位級別 | 資料庫保持 positionLevel |
| `deptManager` | `df_managerTitle` | 部門主管職稱 | 資料庫改為 managerTitle |
| `deptFunctionEffectiveDate` | `df_effectiveDate` | 生效日期 | 必填欄位,資料庫改為 effectiveDate |
| `deptHeadcount` | `df_headcountLimit` | 部門人數上限 | 資料庫改為 headcountLimit |
| `deptStatus` | `df_status` | 部門狀態 | 資料庫改為 status |
| `deptMission` | `df_mission` | 部門使命 | 資料庫改為 mission |
| `deptVision` | `df_vision` | 部門願景 | 資料庫改為 vision |
| `deptCoreFunctions` | `df_coreFunc` | 核心職責 | 必填欄位,資料庫改為 coreFunctions |
| `deptKPIs` | `df_kpis` | 關鍵績效指標 | 資料庫改為 kpis |
| `deptCollaboration` | `df_collab` | 協作部門 | 資料庫改為 collaboration |
| `deptFunctionRemark` | `df_remark` | 備注 | 資料庫改為 remark |
**特別注意:** 原本有兩個事業體欄位 (`deptFunctionBU``deptFunc_businessUnit`),新規範已合併為單一欄位 `df_businessUnit`
---
## 模組 5: 崗位描述 - 基本信息
**需變更3 / 4**
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|-------|-------|---------|------|
| `jd_empNo` | `jd_empNo` | 工號 | ✓ 無需變更 |
| `jd_empName` | `jd_empName` | 姓名 | ✓ 無需變更 |
| `jd_positionCode` | `jd_posCode` | 崗位代碼 | |
| `jd_versionDate` | `jd_versionDate` | 版本更新日期 | ✓ 無需變更 |
---
## 模組 6: 崗位描述 - 崗位基本信息
**需變更10 / 14**
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|-------|-------|---------|------|
| `jd_positionName` | `jd_posName` | 崗位名稱 | |
| `jd_businessUnit` | `jd_businessUnit` | 事業體 | ✓ 無需變更 |
| `jd_division` | `jd_division` | 處級單位 | ✓ 無需變更 |
| `jd_department` | `jd_department` | 部級單位 | ✓ 無需變更 |
| `jd_section` | `jd_section` | 課級單位 | ✓ 無需變更 |
| `jd_positionTitle` | `(刪除)` | 崗位名稱 (重複) | 與 positionName 重複,建議刪除 |
| `jd_positionLevel` | `jd_posLevel` | 崗位級別 | |
| `jd_positionEffectiveDate` | `jd_posEffDate` | 崗位生效日期 | |
| `jd_directSupervisor` | `jd_supervisor` | 直接領導職務 | |
| `jd_positionGradeJob` | `jd_gradeJob` | 崗位職等&職務 | Modal 選擇器 |
| `jd_reportTo` | `jd_reportTo` | 匯報對象職務 | ✓ 無需變更 |
| `jd_directReports` | `jd_directReports` | 直接下級 | ✓ 無需變更 |
| `jd_workLocation` | `jd_location` | 任職地點 | |
| `jd_empAttribute` | `jd_empAttr` | 員工屬性 | |
---
## 模組 7: 崗位描述 - 部門職責資訊
**需變更3 / 5**
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|-------|-------|---------|------|
| `jd_deptFunctionCode` | `jd_dfCode` | 部門職責編號 | readonly |
| `jd_deptFunctionBU` | `(保留)` | 事業體 | 建議保留或改為 jd_dfBU |
| `jd_deptMission` | `jd_deptMission` | 部門使命 | ✓ 無需變更 |
| `jd_deptCoreFunctions` | `jd_deptCoreFunctions` | 部門核心職責 | ✓ 無需變更 |
| `jd_deptKPIs` | `jd_deptKPIs` | 部門 KPIs | ✓ 無需變更 |
**說明:** 這些是 readonly 欄位,自動帶入,可保持現有命名以維持與來源資料的一致性。
---
## 模組 8: 崗位描述 - 職責描述
**需變更1 / 2**
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|-------|-------|---------|------|
| `jd_positionPurpose` | `jd_purpose` | 崗位設置目的 | |
| `jd_mainResponsibilities` | `jd_mainResp` | 主要崗位職責 | Numbered textarea |
---
## 模組 9: 崗位描述 - 崗位要求
**需變更5 / 5**
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|-------|-------|---------|------|
| `jd_education` | `jd_eduLevel` | 教育程度 | |
| `jd_basicSkills` | `jd_basicSkills` | 基本技能 | ✓ 無需變更 |
| `jd_professionalKnowledge` | `jd_proKnowledge` | 專業知識 | |
| `jd_workExperienceReq` | `jd_expReq` | 工作經驗 | |
| `jd_otherRequirements` | `jd_otherReq` | 其他 | |
---
## 重要變更說明
### 1. 前綴標準化
所有欄位統一採用模組前綴:
- `pos_` - 崗位基礎資料
- `rec_` - 招聘要求
- `job_` - 職務基礎資料
- `df_` - 部門職責
- `jd_` - 崗位描述
### 2. 命名簡化
- `positionCode``pos_code`
- `positionName``pos_name`
- `positionCategory``pos_category`
- `positionNature``pos_type` (語義更精確)
- `hasAttendanceBonus``job_hasAttBonus` (縮寫簡化)
### 3. 語義改善
- `titleReq``rec_certReq` (職稱要求 → 證照要求)
- `positionNature``pos_type` (性質 → 類型,更符合英文語義)
- `workExperience``rec_expYears` (經驗年數更明確)
### 4. 資料庫欄位同步變更
部分 HTML ID 變更時,資料庫欄位名稱也需要對應修改:
- `positionNature``positionType`
- `positionDesc``description`
- `positionRemark``remark`
- `minEducation``educationLevel`
### 5. 重複欄位合併
部門職責模組中的兩個事業體欄位:
- 原:`deptFunctionBU``deptFunc_businessUnit`
- 新:統一為 `df_businessUnit`
---
## 實施建議
### 階段 1: 資料庫遷移
1. 執行資料庫欄位重命名 SQL 腳本
2. 更新所有 ORM Model 定義
3. 更新 API 回應的欄位名稱
### 階段 2: 前端更新
1. 全局搜尋替換 HTML ID
2. 更新所有 JavaScript 選擇器
3. 更新 CSS 樣式選擇器
4. 更新表單驗證邏輯
### 階段 3: 測試驗證
1. 單元測試:驗證所有表單欄位綁定
2. 整合測試:驗證資料儲存與讀取
3. E2E 測試:完整流程測試
4. 回歸測試:確保無遺漏欄位
### 階段 4: 文檔更新
1. API 文檔更新
2. 資料字典更新
3. 開發指南更新
4. 使用者手冊更新(如有影響)
---
## 風險評估
### 高風險變更 (必填欄位)
- `positionCode``pos_code`
- `positionName``pos_name`
- `jobCode``job_code`
- `jobName``job_name`
- `deptFunctionCode``df_code`
**建議:** 優先測試這些欄位的儲存與驗證邏輯。
### 中風險變更 (聯動欄位)
- 組織結構欄位:`businessUnit`, `division`, `department`, `section`
- 自動帶出欄位:`positionCategoryName`, `positionNatureName`
**建議:** 仔細測試聯動邏輯和 onchange 事件。
### 低風險變更 (選填欄位)
- 備注類欄位
- 描述類欄位
---
## 檢查清單
- [ ] 資料庫遷移腳本已準備
- [ ] Model 定義已更新
- [ ] HTML 表單 ID 已更新
- [ ] JavaScript 選擇器已更新
- [ ] CSS 樣式已更新
- [ ] API 端點已更新
- [ ] 驗證邏輯已更新
- [ ] 單元測試已通過
- [ ] 整合測試已通過
- [ ] E2E 測試已通過
- [ ] 文檔已更新
- [ ] 程式碼審查已完成
---
**文檔版本:** v1.0
**建立日期:** 2025-12-06
**最後更新:** 2025-12-06
**維護者:** 系統開發團隊

View File

@@ -1,8 +1,8 @@
# HR 基礎資料維護系統 - 軟體設計文件 (SDD) # HR 基礎資料維護系統 - 軟體設計文件 (SDD)
**文件版本**2.1 **文件版本**3.1
**建立日期**2024-12-03 **建立日期**2024-12-03
**最後更新**2024-12-04 **最後更新**2024-12-08
**文件狀態**Released **文件狀態**Released
--- ---
@@ -15,15 +15,17 @@
### 1.2 範圍 ### 1.2 範圍
本系統涵蓋以下三大功能模組: 本系統涵蓋以下功能模組:
| 模組 | 功能說明 | | 模組 | 功能說明 |
|------|----------| |------|----------|
| 崗位基礎資料 | 崗位主檔維護,含基礎資料與招聘要求 | | 崗位基礎資料 | 崗位主檔維護,含基礎資料與招聘要求 |
| 職務基礎資料 | 職務類別與屬性設定維護 | | 職務基礎資料 | 職務類別與屬性設定維護 |
| 部門職責 | 部門職責定義、使命願景與 KPI 維護 |
| 崗位描述 | 職責描述、崗位要求與任職條件維護 | | 崗位描述 | 職責描述、崗位要求與任職條件維護 |
| 崗位清單 | 顯示所有崗位資料,支援查看描述與匯出 | | 崗位清單 | 顯示所有崗位資料,支援查看描述與匯出 |
| 管理者頁面 | 使用者管理與完整崗位資料匯出 | | 管理者頁面 | 使用者管理與完整崗位資料匯出 |
| 組織階層管理 | 事業體、處級單位、部級單位、崗位的層級關聯管理 |
### 1.3 參考文件 ### 1.3 參考文件
@@ -37,8 +39,14 @@
|------|------| |------|------|
| 崗位 (Position) | 組織架構中的職位單位,具有編號、級別、編制人數等屬性 | | 崗位 (Position) | 組織架構中的職位單位,具有編號、級別、編制人數等屬性 |
| 職務 (Job Title) | 職務類別分類,如管理職、技術職、業務職等 | | 職務 (Job Title) | 職務類別分類,如管理職、技術職、業務職等 |
| 部門職責 (DeptFunction) | 部門的職責範圍、使命願景與績效指標定義 |
| 崗位描述 (Job Description) | 詳細描述崗位職責、要求與任職條件的文件 | | 崗位描述 (Job Description) | 詳細描述崗位職責、要求與任職條件的文件 |
| 事業體 (Business Unit) | 組織最高層級,代表獨立經營單位 |
| 處級單位 (Division) | 事業體下的次級組織單位 |
| 部級單位 (Department) | 處級單位下的基層組織單位 |
| AI 自動填充 | 利用大型語言模型自動生成表單欄位內容的功能 | | AI 自動填充 | 利用大型語言模型自動生成表單欄位內容的功能 |
| 三個錦囊 | AI 智能功能區塊,提供自動補齊、範例模板、驗證檢查 |
| LLM | Large Language Model大型語言模型 |
--- ---
@@ -47,65 +55,117 @@
### 2.1 整體架構圖 ### 2.1 整體架構圖
``` ```
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────
│ 使用者介面層 (UI Layer) │ │ 使用者介面層 (UI Layer) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 崗位基礎資料 │ │ 職務基礎資料 │ │ 崗位描述 │ │ 崗位基礎 │ 職務基礎 │ │ 部門職責 崗位描述
│ │ 模組 │ 模組 模組 │ │ │ │ 模組 │ 模組 │ 模組 模組 │
│ └─────────────┘ └─────────────┘ └─────────────┘ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │ ┌───────────────────────────────────────────────────────────────┐ │
│ │ AI 自動填充服務 (Claude API) │ │ 三個錦囊 AI 功能區 │ │
└────────────────────────────────────────────────────────── │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
└─────────────────────────────────────────────────────────────────┘ │ │ │ 自動補齊 │ │ 範例模板 │ │ 驗證檢查 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ ES6 JavaScript 模組化架構 │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ main.js│ │ ui.js │ │ api.js │ │ai-bags │ │utils.js│ │ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────
│ 應用服務層 (Application Layer) │ │ 應用服務層 (Application Layer) │
│ ┌─────────────────────────────────────────────────────────┐ │ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Flask RESTful API │ │ │ │ Flask RESTful API │ │
│ │ ┌───────────┐ ┌────────── ┌───────────┐ │ │ ┌────────── ┌──────────┐ ──────────┐ ┌──────────┐ │ │
│ │ │ Position │ Job │ │ Reference │ │ │ │ │Position │ Job │ │DeptFunc │ │ LLM │ │Hierarchy│ │
│ │ │ API │ API │ API │ │ │ │ API │ API │ API │ API │ │ API │ │
│ │ └───────────┘ └────────── └───────────┘ │ │ │ └────────── └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
│ └─────────────────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘ │ │ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ LLM 配置模組 (llm_config.py) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Gemini │ │DeepSeek │ │ OpenAI │ │ Ollama │ │ GPT-OSS │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────
│ 資料存取層 (Data Layer) │ │ 資料存取層 (Data Layer) │
│ ┌─────────────────────────────────────────────────────────┐ │ ┌───────────────────────────────────────────────────────────────┐ │
│ │ In-Memory Database / Future DB │ │ In-Memory Database / MySQL │ │
│ │ (positions_db, jobs_db, job_desc_db) │ │ │ ┌─────────────────┐ ┌─────────────────────────────────────┐ │
└─────────────────────────────────────────────────────────┘ │ │ positions_db │ │ 組織階層資料 │ │
└─────────────────────────────────────────────────────────────────┘ │ │ │ jobs_db │ │ (business_units, divisions, │ │ │
│ │ │ dept_func_db │ │ departments, org_positions) │ │ │
│ │ │ job_desc_db │ │ │ │ │
│ │ └─────────────────┘ └─────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
``` ```
### 2.2 技術堆疊 ### 2.2 技術堆疊
| 層級 | 技術選型 | 說明 | | 層級 | 技術選型 | 說明 |
|------|----------|------| |------|----------|------|
| 前端 | HTML5 + CSS3 + JavaScript | 純前端實作,無框架依賴 | | 前端 | HTML5 + CSS3 + ES6 JavaScript | 模組化架構,使用 import/export |
| 樣式 | Custom CSS + Google Fonts | Noto Sans TC 字型、CSS Variables | | 樣式 | CSS Modules + Google Fonts | Noto Sans TC 字型、CSS Variables |
| 後端 | Python Flask | RESTful API 服務 | | 後端 | Python Flask | RESTful API 服務 |
| AI 服務 | Claude API (Anthropic) | 智能表單填充 | | AI 服務 | 多 LLM 支援 | Ollama、Gemini、DeepSeek、OpenAI、GPT-OSS |
| 資料庫 | In-Memory (Dict) | 可擴展至 MySQL/PostgreSQL | | 資料庫 | In-Memory + MySQL | 組織階層資料支援 MySQL 持久化 |
### 2.3 部署架 ### 2.3 前端模組結
| 檔案 | 功能說明 |
|------|----------|
| js/main.js | 應用程式入口,初始化與事件綁定 |
| js/ui.js | UI 互動邏輯,表單處理與頁籤切換 |
| js/api.js | API 呼叫封裝,與後端通訊 |
| js/ai-bags.js | 三個錦囊 AI 功能實現 |
| js/utils.js | 工具函數Toast、錯誤處理等 |
| js/config.js | 配置常數API 端點等 |
### 2.4 CSS 模組結構
| 檔案 | 功能說明 |
|------|----------|
| styles/base.css | 基礎樣式、CSS 變數、reset |
| styles/layout.css | 頁面佈局、header、sidebar |
| styles/components.css | 按鈕、表單、卡片、Modal 等元件 |
| styles/modules.css | 各模組特定樣式 |
| styles/responsive.css | 響應式設計樣式 |
### 2.5 部署架構
``` ```
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────
│ 使用者瀏覽器 │ │ 使用者瀏覽器 │
│ ┌─────────────────────────────────┐ │ ┌─────────────────────────────────────────────────────────┐ │
│ │ index.html (前端應用) │ │ │ index.html (前端應用ES6 Modules) │
───────────────────────────────── │ ┌──────────────────────────────────────────────────┐ │
└─────────────────────────────────────────┘ │ │ │ js/main.js → ui.js → api.js → ai-bags.js │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌─────────────────────────┐ ┌────────────────────────────────────┐
│ Flask Server │ │ Claude API │ Flask Server │ │ LLM API Services
│ localhost:5000 │ │ api.anthropic │ localhost:5000 │ │ ┌──────────┐ ┌──────────┐
───────────────────┘ └───────────────────┘ │ ┌───────────────────┐ │ │ │ Ollama │ │ Gemini │ │
│ │ app.py │ │ │ │localhost │ │ Google │ │
│ │ llm_config.py │ │ │ └──────────┘ └──────────┘ │
│ └───────────────────┘ │ │ ┌──────────┐ ┌──────────┐ │
└─────────────────────────┘ │ │ DeepSeek │ │ OpenAI │ │
│ └──────────┘ └──────────┘ │
└────────────────────────────────────┘
``` ```
--- ---
@@ -227,13 +287,51 @@
--- ---
### 3.3 崗位描述模組 ### 3.3 部門職責模組
#### 3.3.1 模組概述 #### 3.3.1 模組概述
提供部門職責定義維護功能,包含部門使命願景、核心職責與績效指標設定。
#### 3.3.2 欄位規格
| 欄位名稱 | 欄位ID | 資料類型 | 必填 | 說明 |
|----------|--------|----------|------|------|
| 職責編號 | df_code | String(20) | ✓ | 格式 DF-001 |
| 職責名稱 | df_name | String(100) | ✓ | |
| 事業體 | df_businessUnit | Enum | ✓ | SBU/MBU/TBU |
| 處級單位 | df_division | Enum | ✓ | 聯動選項 |
| 部級單位 | df_department | Enum | ✓ | 聯動選項 |
| 課級單位 | df_section | String(50) | | |
| 對應崗位 | df_posTitle | Enum | ✓ | 關聯 Position |
| 崗位級別 | df_posLevel | Enum | | L1-L7 |
| 部門主管職稱 | df_managerTitle | String(100) | | |
| 生效日期 | df_effectiveDate | Date | ✓ | |
| 人數上限 | df_headcountLimit | Integer | | |
| 狀態 | df_status | Enum | | active/inactive |
| 部門使命 | df_mission | Text | | |
| 部門願景 | df_vision | Text | | |
| 核心職責 | df_coreFunc | Text | ✓ | |
| KPIs | df_kpis | Text | | 績效指標 |
| 協作部門 | df_collab | Text | | |
| 備注 | df_remark | Text | | |
#### 3.3.3 狀態代碼對照表
| 代碼 | 名稱 |
|------|------|
| active | 生效中 |
| inactive | 已停用 |
---
### 3.4 崗位描述模組
#### 3.4.1 模組概述
提供完整的崗位描述書 (Job Description) 維護功能,包含崗位基本信息、職責描述與崗位要求三大區塊。 提供完整的崗位描述書 (Job Description) 維護功能,包含崗位基本信息、職責描述與崗位要求三大區塊。
#### 3.3.2 頂部區域欄位 #### 3.4.2 頂部區域欄位
| 欄位名稱 | 欄位ID | 資料類型 | 說明 | | 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|----------|--------|----------|------| |----------|--------|----------|------|
@@ -242,7 +340,7 @@
| 崗位代碼 | jd_positionCode | String(20) | | | 崗位代碼 | jd_positionCode | String(20) | |
| 版本更新日期 | jd_versionDate | Date | | | 版本更新日期 | jd_versionDate | Date | |
#### 3.3.3 崗位基本信息區塊 #### 3.4.3 崗位基本信息區塊
| 欄位名稱 | 欄位ID | 資料類型 | 說明 | | 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|----------|--------|----------|------| |----------|--------|----------|------|
@@ -256,14 +354,14 @@
| 任職地點 | jd_workLocation | Enum | HQ/TPE/TYC/KHH/SH/SZ | | 任職地點 | jd_workLocation | Enum | HQ/TPE/TYC/KHH/SH/SZ |
| 員工屬性 | jd_empAttribute | Enum | FT/CT/PT/IN/DP | | 員工屬性 | jd_empAttribute | Enum | FT/CT/PT/IN/DP |
#### 3.3.4 職責描述區塊 #### 3.4.4 職責描述區塊
| 欄位名稱 | 欄位ID | 資料類型 | 說明 | | 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|----------|--------|----------|------| |----------|--------|----------|------|
| 崗位設置目的 | jd_positionPurpose | String(500) | 單行文字 | | 崗位設置目的 | jd_positionPurpose | String(500) | 單行文字 |
| 主要崗位職責 | jd_mainResponsibilities | Text | 編號格式 1、2、3、... | | 主要崗位職責 | jd_mainResponsibilities | Text | 編號格式 1、2、3、... |
#### 3.3.5 崗位要求區塊 #### 3.4.5 崗位要求區塊
| 欄位名稱 | 欄位ID | 資料類型 | 說明 | | 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|----------|--------|----------|------| |----------|--------|----------|------|
@@ -273,7 +371,7 @@
| 工作經驗 | jd_workExperienceReq | Text | | | 工作經驗 | jd_workExperienceReq | Text | |
| 其他 | jd_otherRequirements | Text | | | 其他 | jd_otherRequirements | Text | |
#### 3.3.6 任職地點代碼對照表 #### 3.4.6 任職地點代碼對照表
| 代碼 | 名稱 | | 代碼 | 名稱 |
|------|------| |------|------|
@@ -284,7 +382,7 @@
| SH | 上海辦公室 | | SH | 上海辦公室 |
| SZ | 深圳辦公室 | | SZ | 深圳辦公室 |
#### 3.3.7 員工屬性代碼對照表 #### 3.4.7 員工屬性代碼對照表
| 代碼 | 名稱 | | 代碼 | 名稱 |
|------|------| |------|------|
@@ -358,6 +456,18 @@
| GET | `/api/reference/education` | 學歷選項 | | GET | `/api/reference/education` | 學歷選項 |
| GET | `/api/reference/majors` | 專業選項 | | GET | `/api/reference/majors` | 專業選項 |
#### 4.1.7 組織階層 API
| 方法 | 端點 | 說明 |
|------|------|------|
| GET | `/api/hierarchy/business-units` | 獲取所有事業體 |
| GET | `/api/hierarchy/divisions` | 獲取處級單位(可按事業體篩選) |
| GET | `/api/hierarchy/departments` | 獲取部級單位(可按處級單位篩選) |
| GET | `/api/hierarchy/positions` | 獲取崗位名稱(可按部級單位篩選) |
| GET | `/api/hierarchy/full` | 獲取完整階層資料(支援分頁) |
| GET | `/api/hierarchy/cascade` | 獲取級聯選擇資料 |
| GET | `/api/hierarchy/stats` | 獲取組織統計資訊 |
### 4.2 API 請求/回應範例 ### 4.2 API 請求/回應範例
#### 4.2.1 新增崗位 #### 4.2.1 新增崗位
@@ -448,13 +558,51 @@ GET /api/positions?page=1&size=20&search=工程師
--- ---
## 5. AI 自動填充功能設計 ## 5. AI 智能功能設計
### 5.1 功能概述 ### 5.1 功能概述
系統整合 Claude API提供智能表單填充功能。用戶點擊「✨ I'm feeling lucky」按鈕後AI 會根據已填寫的內容,自動補充尚未填寫的欄位。 系統整合多種 LLM API提供「三個錦囊」智能功能區塊
### 5.2 填充邏輯 | 錦囊 | 功能 | 說明 |
|------|------|------|
| 自動補齊 | AI 自動填充 | 根據已填內容智能生成其餘欄位 |
| 範例模板 | 範例資料生成 | 提供符合行業標準的範例資料 |
| 驗證檢查 | 資料驗證建議 | 檢查資料完整性與合理性 |
### 5.2 支援的 LLM 服務
| 服務 | 配置鍵 | 預設模型 | 說明 |
|------|--------|----------|------|
| Ollama | OLLAMA_API_KEY | qwen2.5:3b | 本地部署,無需 API 費用 |
| Gemini | GEMINI_API_KEY | gemini-pro | Google AI 服務 |
| DeepSeek | DEEPSEEK_API_KEY | deepseek-chat | 中文優化模型 |
| OpenAI | OPENAI_API_KEY | gpt-3.5-turbo | OpenAI 官方服務 |
| GPT-OSS | GPTOSS_API_KEY | - | 開源替代服務 |
### 5.3 三個錦囊 UI 設計
```
┌─────────────────────────────────────────────────────┐
│ 三個錦囊 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 自動補齊 │ │ 範例模板 │ │ 驗證檢查 │ │
│ │ AI │ │ Template │ │ Validate │ │
│ │ (Edit) │ │ (Edit) │ │ (Edit) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────┘
```
### 5.4 Prompt 編輯功能
每個錦囊支援 Prompt 編輯,用戶可自訂 AI 生成邏輯:
- 標題:錦囊顯示名稱
- 副標題:功能簡述
- Prompt發送給 LLM 的指令內容
- 儲存至 LocalStorage持久化設定
### 5.5 填充邏輯
``` ```
┌─────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────┐
@@ -489,9 +637,9 @@ GET /api/positions?page=1&size=20&search=工程師
└─────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────┘
``` ```
### 5.3 核心函數 ### 5.6 核心函數
#### 5.3.1 fillIfEmpty #### 5.6.1 fillIfEmpty
```javascript ```javascript
/** /**
@@ -510,7 +658,7 @@ function fillIfEmpty(elementId, value) {
} }
``` ```
#### 5.3.2 getEmptyFields #### 5.6.2 getEmptyFields
```javascript ```javascript
/** /**
@@ -523,28 +671,24 @@ function getEmptyFields(fieldIds) {
} }
``` ```
### 5.4 API 呼叫規格 ### 5.7 API 呼叫規格
**Endpoint** **本地 Flask API Endpoint**
``` ```
POST https://api.anthropic.com/v1/messages POST http://localhost:5000/api/llm/generate
``` ```
**Request Body** **Request Body**
```json ```json
{ {
"model": "claude-sonnet-4-20250514", "api": "ollama",
"max_tokens": 2000, "model": "qwen2.5:3b",
"messages": [ "prompt": "Prompt 內容...",
{ "max_tokens": 2000
"role": "user",
"content": "Prompt 內容..."
}
]
} }
``` ```
### 5.5 Prompt 設計範例 ### 5.8 Prompt 設計範例
``` ```
請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。 請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
@@ -741,6 +885,105 @@ interface PositionListItem {
} }
``` ```
### 7.5 組織階層資料結構
#### 7.5.1 事業體 (BusinessUnit)
```typescript
interface BusinessUnit {
id: number; // 主鍵
business_code: string; // 事業體代碼 (BU001, BU002...)
business_name: string; // 事業體名稱
sort_order: number; // 排序
is_active: boolean; // 是否啟用
remark: string; // 備註
created_at: string; // 建立時間
updated_at: string; // 更新時間
}
```
#### 7.5.2 處級單位 (Division)
```typescript
interface Division {
id: number; // 主鍵
division_code: string; // 處級單位代碼 (DIV001, DIV002...)
division_name: string; // 處級單位名稱
business_id: number; // 所屬事業體ID (FK)
sort_order: number; // 排序
is_active: boolean; // 是否啟用
remark: string; // 備註
created_at: string; // 建立時間
updated_at: string; // 更新時間
}
```
#### 7.5.3 部級單位 (Department)
```typescript
interface Department {
id: number; // 主鍵
department_code: string; // 部級單位代碼 (DEPT001, DEPT002...)
department_name: string; // 部級單位名稱
division_id: number; // 所屬處級單位ID (FK)
sort_order: number; // 排序
is_active: boolean; // 是否啟用
remark: string; // 備註
created_at: string; // 建立時間
updated_at: string; // 更新時間
}
```
#### 7.5.4 組織崗位關聯 (OrganizationPosition)
```typescript
interface OrganizationPosition {
id: number; // 主鍵
business_id: number; // 事業體ID (FK)
division_id: number; // 處級單位ID (FK)
department_id: number; // 部級單位ID (FK)
position_title: string; // 崗位名稱
sort_order: number; // 排序
is_active: boolean; // 是否啟用
created_at: string; // 建立時間
updated_at: string; // 更新時間
}
```
#### 7.5.5 組織階層關係圖
```
事業體 (business_units)
├── 岡山製造事業體
│ ├── 生產處 (divisions)
│ │ ├── 生產部 (departments)
│ │ │ ├── 經副理 (organization_positions)
│ │ │ ├── 課長
│ │ │ ├── 組長
│ │ │ └── 班長
│ │ └── 生產企劃部
│ │ ├── 經副理
│ │ └── 專員
│ ├── 封裝工程處
│ └── ...
├── 產品事業體
│ ├── 先進產品事業處
│ └── 成熟產品事業處
└── ...(共 15 個事業體)
```
#### 7.5.6 資料統計
| 層級 | 資料筆數 |
|------|----------|
| 事業體 | 15 筆 |
| 處級單位 | ~45 筆 |
| 部級單位 | ~70 筆 |
| 組織崗位關聯 | ~350 筆(去重後) |
--- ---
## 8. 快捷鍵設計 ## 8. 快捷鍵設計
@@ -758,9 +1001,35 @@ interface PositionListItem {
hr_position_form/ hr_position_form/
├── index.html # 前端應用主檔 ├── index.html # 前端應用主檔
├── app.py # Flask 後端 API ├── app.py # Flask 後端 API
├── llm_config.py # LLM API 配置模組
├── database_schema.sql # MySQL 資料庫結構定義
├── init_database.py # 資料庫初始化腳本
├── import_hierarchy_data.py # 組織階層資料匯入腳本
├── hierarchical_data.js # 組織階層原始資料
├── requirements.txt # Python 依賴套件 ├── requirements.txt # Python 依賴套件
├── .env # 環境變數配置 (API Keys)
├── README.md # 使用說明文件 ├── README.md # 使用說明文件
── SDD.md # 軟體設計文件(本文件) ── SDD.md # 軟體設計文件
├── TDD.md # 測試設計文件
├── js/ # JavaScript 模組
│ ├── main.js # 應用程式入口
│ ├── ui.js # UI 互動邏輯
│ ├── api.js # API 呼叫封裝
│ ├── ai-bags.js # 三個錦囊功能
│ ├── utils.js # 工具函數
│ └── config.js # 配置常數
├── styles/ # CSS 樣式模組
│ ├── base.css # 基礎樣式
│ ├── layout.css # 頁面佈局
│ ├── components.css # UI 元件
│ ├── modules.css # 模組樣式
│ └── responsive.css # 響應式設計
└── data/ # 資料檔案
├── positions.csv # 崗位資料範本
└── jobs.csv # 職務資料範本
``` ```
--- ---
@@ -837,6 +1106,8 @@ const i18n = {
| 1.0 | 2024-12-03 | System | 初始版本,包含三大模組設計與 AI 功能 | | 1.0 | 2024-12-03 | System | 初始版本,包含三大模組設計與 AI 功能 |
| 2.0 | 2024-12-04 | System | 新增 MySQL 資料庫整合、多 LLM API 支援、全局錯誤處理、Gitea 版本控制 | | 2.0 | 2024-12-04 | System | 新增 MySQL 資料庫整合、多 LLM API 支援、全局錯誤處理、Gitea 版本控制 |
| 2.1 | 2024-12-04 | System | 新增崗位描述保存功能、崗位清單模組、管理者頁面匯出功能、CSV 批次匯入 | | 2.1 | 2024-12-04 | System | 新增崗位描述保存功能、崗位清單模組、管理者頁面匯出功能、CSV 批次匯入 |
| 3.0 | 2024-12-06 | System | 新增部門職責模組、三個錦囊 AI 功能、ES6 模組化架構、改進 JSON 解析錯誤處理 |
| 3.1 | 2024-12-08 | System | 新增組織階層管理模組,包含事業體、處級單位、部級單位、崗位的四層架構;新增 7 個組織階層 API 端點;更新資料庫結構支援組織階層資料 |
--- ---

File diff suppressed because it is too large Load Diff

496
docs/TDD.md Normal file
View File

@@ -0,0 +1,496 @@
# HR 基礎資料維護系統 - 測試設計文件 (TDD)
**文件版本**1.1
**建立日期**2024-12-06
**最後更新**2025-12-08
**文件狀態**Draft
---
## 1. 文件概述
### 1.1 目的
本文件為 HR 基礎資料維護系統之測試設計文件 (Test Design Document),描述測試策略、測試範圍、測試案例設計與驗收標準,供測試人員與開發人員參考。
### 1.2 範圍
本測試涵蓋以下功能模組:
| 模組 | 測試重點 |
|------|----------|
| 崗位基礎資料 | CRUD 操作、欄位驗證、資料聯動 |
| 職務基礎資料 | CRUD 操作、類別關聯、Toggle 控件 |
| 部門職責 | CRUD 操作、組織層級聯動 |
| 崗位描述 | CRUD 操作、版本控制 |
| AI 幫我想 | LLM 呼叫、JSON 解析、Prompt 編輯、結果填入 |
| 崗位清單 | 列表顯示、搜尋過濾、資料匯出 |
| 組織階層 | 事業體/處級/部級單位、級聯選擇、API 查詢 |
| CSV 匯入匯出 | 批次匯入、範本下載、資料驗證 |
### 1.3 參考文件
- 軟體設計文件 (SDD.md)
- API 設計規範
- 需求確認文件
---
## 2. 測試策略
### 2.1 測試層次
```
┌─────────────────────────────────────────────────────────┐
│ E2E 測試 (End-to-End) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 整合測試 (Integration) │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ 單元測試 (Unit) │ │ │
│ │ │ ┌─────────────────────────────────┐ │ │ │
│ │ │ │ 函數 / API / 元件 │ │ │ │
│ │ │ └─────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
### 2.2 測試類型
| 類型 | 說明 | 工具 |
|------|------|------|
| 單元測試 | 測試單一函數或模組 | Jest / pytest |
| 整合測試 | 測試模組間互動 | pytest |
| API 測試 | 測試 RESTful API | Postman / pytest |
| UI 測試 | 測試使用者介面 | 手動測試 / Selenium |
| 效能測試 | 測試系統效能 | Apache JMeter |
### 2.3 測試環境
| 環境 | 配置 |
|------|------|
| 前端 | Chrome/Firefox/Edge 最新版 |
| 後端 | Python 3.9+, Flask 2.0+ |
| LLM | Ollama localhost:11434 |
| 資料庫 | In-Memory (測試環境) |
---
## 3. 功能測試案例
### 3.1 崗位基礎資料模組
#### 3.1.1 新增崗位
| TC-ID | TC-POS-001 |
|-------|------------|
| 測試名稱 | 新增崗位 - 正常流程 |
| 前置條件 | 系統已啟動,用戶在崗位基礎資料頁面 |
| 測試步驟 | 1. 輸入崗位編號 "ENG-001"<br>2. 輸入崗位名稱 "資深軟體工程師"<br>3. 選擇崗位類別 "技術職"<br>4. 點擊「保存」按鈕 |
| 預期結果 | 顯示「保存成功」Toast資料寫入成功 |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-POS-002 |
|-------|------------|
| 測試名稱 | 新增崗位 - 必填欄位驗證 |
| 前置條件 | 系統已啟動,用戶在崗位基礎資料頁面 |
| 測試步驟 | 1. 不輸入崗位編號<br>2. 不輸入崗位名稱<br>3. 點擊「保存」按鈕 |
| 預期結果 | 顯示錯誤訊息「請填寫必填欄位」 |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-POS-003 |
|-------|------------|
| 測試名稱 | 新增崗位 - 編號重複檢查 |
| 前置條件 | 已存在編號 "ENG-001" 的崗位 |
| 測試步驟 | 1. 輸入崗位編號 "ENG-001"<br>2. 輸入其他欄位<br>3. 點擊「保存」按鈕 |
| 預期結果 | 顯示錯誤訊息「崗位編號已存在」 |
| 實際結果 | |
| 通過/失敗 | |
#### 3.1.2 編輯崗位
| TC-ID | TC-POS-004 |
|-------|------------|
| 測試名稱 | 編輯崗位 - 正常流程 |
| 前置條件 | 已存在崗位 "ENG-001" |
| 測試步驟 | 1. 瀏覽至崗位 "ENG-001"<br>2. 修改崗位名稱為 "高級軟體工程師"<br>3. 點擊「保存」按鈕 |
| 預期結果 | 顯示「更新成功」Toast資料已更新 |
| 實際結果 | |
| 通過/失敗 | |
#### 3.1.3 刪除崗位
| TC-ID | TC-POS-005 |
|-------|------------|
| 測試名稱 | 刪除崗位 - 正常流程 |
| 前置條件 | 已存在崗位 "ENG-001" |
| 測試步驟 | 1. 瀏覽至崗位 "ENG-001"<br>2. 點擊「刪除」按鈕<br>3. 確認刪除 |
| 預期結果 | 顯示「刪除成功」Toast資料已移除 |
| 實際結果 | |
| 通過/失敗 | |
### 3.2 職務基礎資料模組
| TC-ID | TC-JOB-001 |
|-------|------------|
| 測試名稱 | 新增職務 - 正常流程 |
| 前置條件 | 系統已啟動,用戶在職務基礎資料頁面 |
| 測試步驟 | 1. 選擇職務類別 "技術職"<br>2. 輸入職務編號 "TECH-001"<br>3. 輸入職務名稱 "軟體工程師"<br>4. 點擊「保存」按鈕 |
| 預期結果 | 顯示「保存成功」Toast |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-JOB-002 |
|-------|------------|
| 測試名稱 | Toggle 開關功能 |
| 前置條件 | 系統已啟動,用戶在職務基礎資料頁面 |
| 測試步驟 | 1. 開啟「是否有全勤獎金」Toggle<br>2. 開啟「是否住房補貼」Toggle<br>3. 保存資料 |
| 預期結果 | Toggle 狀態正確保存,重新載入後狀態一致 |
| 實際結果 | |
| 通過/失敗 | |
### 3.3 部門職責模組
| TC-ID | TC-DF-001 |
|-------|------------|
| 測試名稱 | 新增部門職責 - 正常流程 |
| 前置條件 | 系統已啟動,用戶在部門職責頁面 |
| 測試步驟 | 1. 輸入職責編號 "DF-001"<br>2. 輸入職責名稱 "研發部門職責"<br>3. 選擇事業體、處級單位、部級單位<br>4. 輸入核心職責<br>5. 點擊「保存」按鈕 |
| 預期結果 | 顯示「保存成功」Toast |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-DF-002 |
|-------|------------|
| 測試名稱 | 組織層級聯動 |
| 前置條件 | 系統已啟動 |
| 測試步驟 | 1. 選擇事業體 "SBU"<br>2. 觀察處級單位選項變化<br>3. 選擇處級單位<br>4. 觀察部級單位選項變化 |
| 預期結果 | 各層級選項正確聯動,無無效選項 |
| 實際結果 | |
| 通過/失敗 | |
### 3.4 AI 幫我想功能
> **說明**v1.1 版本將三個錦囊按鈕整合為單一「AI 幫我想」按鈕,並提供可編輯的 Prompt 編輯器。
| TC-ID | TC-AI-001 |
|-------|------------|
| 測試名稱 | AI 幫我想 - 正常流程 |
| 前置條件 | Ollama 服務已啟動,用戶在崗位基礎資料頁面 |
| 測試步驟 | 1. 查看預設 Prompt 內容<br>2. 點擊「AI 幫我想」按鈕<br>3. 等待 AI 生成完成 |
| 預期結果 | AI 生成資料並填充至表單空白欄位顯示「AI 生成完成已填入表單」Toast |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-AI-002 |
|-------|------------|
| 測試名稱 | Prompt 編輯功能 |
| 前置條件 | 系統已啟動 |
| 測試步驟 | 1. 在 Prompt 編輯器修改內容<br>2. 點擊「AI 幫我想」按鈕<br>3. 重新整理頁面<br>4. 檢查 Prompt 是否保留 |
| 預期結果 | 修改的 Prompt 保存至 LocalStorage重新整理後恢復 |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-AI-003 |
|-------|------------|
| 測試名稱 | Prompt 重置功能 |
| 前置條件 | 已修改 Prompt 內容 |
| 測試步驟 | 1. 修改 Prompt 內容<br>2. 點擊重置按鈕(↺ 圖示) |
| 預期結果 | Prompt 恢復為預設值,顯示「已重置為預設 Prompt」Toast |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-AI-004 |
|-------|------------|
| 測試名稱 | 各模組 AI 功能 |
| 前置條件 | Ollama 服務已啟動 |
| 測試步驟 | 分別在以下模組測試 AI 功能:<br>1. 崗位基礎資料 (positionBasic)<br>2. 招聘要求 (positionRecruit)<br>3. 職務基礎資料 (jobBasic)<br>4. 部門職責 (deptFunction)<br>5. 崗位描述 (jobDesc) |
| 預期結果 | 各模組 AI 均能正常生成對應欄位資料 |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-AI-005 |
|-------|------------|
| 測試名稱 | JSON 解析錯誤處理 |
| 前置條件 | LLM 返回非標準 JSON 格式(含思考過程) |
| 測試步驟 | 使用 deepseek-reasoner 等推理模型,觀察 JSON 解析 |
| 預期結果 | 系統正確從回應中提取 JSON 物件,忽略思考過程文字 |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-AI-006 |
|-------|------------|
| 測試名稱 | 按鈕載入狀態 |
| 前置條件 | 系統已啟動 |
| 測試步驟 | 1. 點擊「AI 幫我想」按鈕<br>2. 觀察按鈕狀態變化 |
| 預期結果 | 按鈕顯示 spinner 和「AI 生成中...」文字,完成後恢復原狀 |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-AI-007 |
|-------|------------|
| 測試名稱 | 空白 Prompt 防護 |
| 前置條件 | 系統已啟動 |
| 測試步驟 | 1. 清空 Prompt 編輯器<br>2. 點擊「AI 幫我想」按鈕 |
| 預期結果 | 顯示錯誤訊息「請輸入 Prompt 指令」 |
| 實際結果 | |
| 通過/失敗 | |
### 3.5 CSV 匯入匯出
| TC-ID | TC-CSV-001 |
|-------|------------|
| 測試名稱 | 下載 CSV 範本 |
| 前置條件 | 系統已啟動 |
| 測試步驟 | 1. 點擊「下載崗位 CSV 範本」按鈕 |
| 預期結果 | 瀏覽器下載 CSV 檔案,格式正確 |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-CSV-002 |
|-------|------------|
| 測試名稱 | 匯入崗位 CSV |
| 前置條件 | 已準備符合格式的 CSV 檔案 |
| 測試步驟 | 1. 點擊「匯入崗位」按鈕<br>2. 選擇 CSV 檔案<br>3. 確認匯入 |
| 預期結果 | 顯示匯入結果,資料正確寫入 |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-CSV-003 |
|-------|------------|
| 測試名稱 | 匯入 CSV - 格式錯誤 |
| 前置條件 | CSV 檔案格式不正確 |
| 測試步驟 | 1. 選擇格式錯誤的 CSV 檔案<br>2. 嘗試匯入 |
| 預期結果 | 顯示錯誤訊息,說明格式問題 |
| 實際結果 | |
| 通過/失敗 | |
### 3.6 組織階層模組
| TC-ID | TC-ORG-001 |
|-------|------------|
| 測試名稱 | 事業體列表載入 |
| 前置條件 | 組織階層資料已匯入 |
| 測試步驟 | 1. 開啟部門職責頁面<br>2. 查看事業體下拉選單 |
| 預期結果 | 顯示所有事業體選項SBU, PBU, NBU 等) |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-ORG-002 |
|-------|------------|
| 測試名稱 | 處級單位級聯選擇 |
| 前置條件 | 組織階層資料已匯入 |
| 測試步驟 | 1. 選擇事業體 "SBU"<br>2. 觀察處級單位選項 |
| 預期結果 | 處級單位僅顯示屬於 SBU 的選項 |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-ORG-003 |
|-------|------------|
| 測試名稱 | 部級單位級聯選擇 |
| 前置條件 | 已選擇事業體和處級單位 |
| 測試步驟 | 1. 選擇處級單位<br>2. 觀察部級單位選項 |
| 預期結果 | 部級單位僅顯示屬於該處級單位的選項 |
| 實際結果 | |
| 通過/失敗 | |
| TC-ID | TC-ORG-004 |
|-------|------------|
| 測試名稱 | 崗位名稱級聯選擇 |
| 前置條件 | 已選擇部級單位 |
| 測試步驟 | 1. 選擇部級單位<br>2. 觀察崗位名稱選項 |
| 預期結果 | 崗位名稱僅顯示屬於該部級單位的選項 |
| 實際結果 | |
| 通過/失敗 | |
---
## 4. API 測試案例
### 4.1 崗位 API
| TC-ID | 端點 | 方法 | 測試說明 | 預期狀態碼 |
|-------|------|------|----------|------------|
| TC-API-001 | /api/positions | GET | 查詢所有崗位 | 200 |
| TC-API-002 | /api/positions | POST | 新增崗位(正常) | 201 |
| TC-API-003 | /api/positions | POST | 新增崗位(缺少必填) | 400 |
| TC-API-004 | /api/positions/{id} | GET | 查詢單一崗位 | 200 |
| TC-API-005 | /api/positions/{id} | GET | 查詢不存在崗位 | 404 |
| TC-API-006 | /api/positions/{id} | PUT | 更新崗位 | 200 |
| TC-API-007 | /api/positions/{id} | DELETE | 刪除崗位 | 200 |
### 4.2 職務 API
| TC-ID | 端點 | 方法 | 測試說明 | 預期狀態碼 |
|-------|------|------|----------|------------|
| TC-API-011 | /api/jobs | GET | 查詢所有職務 | 200 |
| TC-API-012 | /api/jobs | POST | 新增職務(正常) | 201 |
| TC-API-013 | /api/jobs/{id} | PUT | 更新職務 | 200 |
| TC-API-014 | /api/jobs/{id} | DELETE | 刪除職務 | 200 |
### 4.3 LLM API
| TC-ID | 端點 | 方法 | 測試說明 | 預期狀態碼 |
|-------|------|------|----------|------------|
| TC-API-021 | /api/llm/generate | POST | LLM 生成Ollama | 200 |
| TC-API-022 | /api/llm/generate | POST | LLM 生成(無效 API | 400 |
| TC-API-023 | /api/llm/generate | POST | LLM 服務不可用 | 500 |
### 4.4 組織階層 API
| TC-ID | 端點 | 方法 | 測試說明 | 預期狀態碼 |
|-------|------|------|----------|------------|
| TC-API-031 | /api/hierarchy/business-units | GET | 查詢所有事業體 | 200 |
| TC-API-032 | /api/hierarchy/divisions | GET | 查詢所有處級單位 | 200 |
| TC-API-033 | /api/hierarchy/divisions?business=SBU | GET | 按事業體篩選處級單位 | 200 |
| TC-API-034 | /api/hierarchy/departments | GET | 查詢所有部級單位 | 200 |
| TC-API-035 | /api/hierarchy/departments?division=技術處 | GET | 按處級篩選部級單位 | 200 |
| TC-API-036 | /api/hierarchy/positions | GET | 查詢所有崗位名稱 | 200 |
| TC-API-037 | /api/hierarchy/positions?department=研發部 | GET | 按部級篩選崗位 | 200 |
| TC-API-038 | /api/hierarchy/full | GET | 查詢完整階層資料 | 200 |
| TC-API-039 | /api/hierarchy/full?page=1&size=50 | GET | 分頁查詢階層資料 | 200 |
| TC-API-040 | /api/hierarchy/cascade | GET | 查詢級聯選擇資料 | 200 |
| TC-API-041 | /api/hierarchy/stats | GET | 查詢組織統計 | 200 |
---
## 5. UI 測試案例
### 5.1 表單互動
| TC-ID | TC-UI-001 |
|-------|------------|
| 測試名稱 | 頁籤切換 |
| 測試步驟 | 1. 點擊「招聘要求資料」頁籤<br>2. 點擊「基礎資料」頁籤 |
| 預期結果 | 頁籤內容正確切換,動畫流暢 |
| TC-ID | TC-UI-002 |
|-------|------------|
| 測試名稱 | 類別聯動顯示 |
| 測試步驟 | 1. 選擇崗位類別 "01"<br>2. 觀察崗位類別名稱欄位 |
| 預期結果 | 自動顯示「技術職」 |
| TC-ID | TC-UI-003 |
|-------|------------|
| 測試名稱 | Toast 通知顯示 |
| 測試步驟 | 1. 執行保存操作 |
| 預期結果 | 右上角顯示 Toast 通知3秒後自動消失 |
### 5.2 響應式設計
| TC-ID | TC-UI-010 |
|-------|------------|
| 測試名稱 | 手機版佈局 |
| 測試步驟 | 將瀏覽器寬度調整為 375px |
| 預期結果 | 表單變為單欄佈局,按鈕仍可操作 |
| TC-ID | TC-UI-011 |
|-------|------------|
| 測試名稱 | 平板版佈局 |
| 測試步驟 | 將瀏覽器寬度調整為 768px |
| 預期結果 | 佈局適當調整,無元素重疊 |
---
## 6. 效能測試
### 6.1 載入效能
| 測試項目 | 目標值 | 測試方法 |
|----------|--------|----------|
| 首次載入時間 | < 3秒 | Chrome DevTools |
| API 回應時間 | < 500ms | Postman |
| LLM 生成時間 | < 30秒 | 實際測試 |
### 6.2 並發測試
| 測試項目 | 目標值 |
|----------|--------|
| 同時連線數 | 50+ |
| API 請求/ | 100+ |
---
## 7. 安全測試
### 7.1 輸入驗證
| TC-ID | TC-SEC-001 |
|-------|------------|
| 測試名稱 | XSS 防護 |
| 測試步驟 | 在輸入框輸入 `<script>alert('XSS')</script>` |
| 預期結果 | 腳本不執行內容被轉義或過濾 |
| TC-ID | TC-SEC-002 |
|-------|------------|
| 測試名稱 | SQL Injection 防護 |
| 測試步驟 | 在輸入框輸入 `'; DROP TABLE positions; --` |
| 預期結果 | SQL 不執行顯示錯誤或過濾 |
### 7.2 API 安全
| TC-ID | TC-SEC-010 |
|-------|------------|
| 測試名稱 | CORS 設定 |
| 測試步驟 | 從不同來源發送 API 請求 |
| 預期結果 | 僅允許白名單來源 |
---
## 8. 測試報告模板
### 8.1 測試摘要
| 項目 | 數值 |
|------|------|
| 總測試案例數 | |
| 通過數 | |
| 失敗數 | |
| 通過率 | |
| 測試日期 | |
| 測試人員 | |
### 8.2 缺陷追蹤
| 缺陷ID | 嚴重度 | 標題 | 狀態 | 負責人 |
|--------|--------|------|------|--------|
| | | | | |
---
## 9. 驗收標準
### 9.1 功能驗收
- [ ] 所有 CRUD 操作正常運作
- [ ] 三個錦囊 AI 功能可正常生成內容
- [ ] CSV 匯入匯出功能完整
- [ ] 所有必填欄位驗證正確
- [ ] 組織層級聯動正確
### 9.2 效能驗收
- [ ] 頁面載入時間 < 3秒
- [ ] API 回應時間 < 500ms
- [ ] 無明顯 UI 卡頓
### 9.3 相容性驗收
- [ ] Chrome 最新版正常
- [ ] Firefox 最新版正常
- [ ] Edge 最新版正常
---
## 10. 版本歷史
| 版本 | 日期 | 作者 | 變更說明 |
|------|------|------|----------|
| 1.0 | 2024-12-06 | System | 初始版本 |
| 1.1 | 2025-12-08 | System | 1. 更新 AI 功能測試案例三個錦囊 AI 幫我想<br>2. 新增組織階層模組測試案例<br>3. 新增組織階層 API 測試案例<br>4. 更新 JSON 解析錯誤處理測試(支援推理模型) |
---
**文件結束**

File diff suppressed because it is too large Load Diff

758
docs/prompt.md Normal file
View File

@@ -0,0 +1,758 @@
# HR 崗位管理系統 - AI 生成功能 Prompt 說明文件
> **文件版本**: v1.0
> **最後更新**: 2024-12-04
> **維護者**: AI所以有問題真的不要問我
---
## 📋 目錄
1. [總覽](#總覽)
2. [頁籤 1: 崗位基礎資料維護](#頁籤-1-崗位基礎資料維護)
3. [頁籤 2: 崗位招聘要求](#頁籤-2-崗位招聘要求)
4. [頁籤 3: 職務基礎資料](#頁籤-3-職務基礎資料)
5. [頁籤 4: 部門職責維護](#頁籤-4-部門職責維護)
6. [頁籤 5: 崗位描述 (JD)](#頁籤-5-崗位描述-jd)
7. [如何修改 Prompt](#如何修改-prompt)
8. [Prompt 設計原則](#prompt-設計原則)
---
## 總覽
系統中共有 **5 個頁籤**提供 AI 自動生成功能,每個頁籤都有一個 "✨ I'm feeling lucky" 按鈕。
### 🎯 核心運作原理
1. **智能空白檢測**: 系統會檢測哪些欄位是空白的
2. **上下文感知**: 將已填寫的欄位作為上下文傳給 LLM
3. **精準生成**: 只生成尚未填寫的欄位
4. **JSON 格式回傳**: 要求 LLM 返回結構化的 JSON 資料
5. **自動填充**: 解析 JSON 並填入對應欄位
### 🔗 對應函式與程式碼位置
| 頁籤名稱 | 函式名稱 | 程式碼位置 | 按鈕 ID/onclick |
|---------|---------|-----------|----------------|
| 崗位基礎資料維護 | `generatePositionBasic()` | [index.html:2179](index.html#L2179) | `onclick="generatePositionBasic()"` |
| 崗位招聘要求 | `generatePositionRecruit()` | [index.html:2253](index.html#L2253) | `onclick="generatePositionRecruit()"` |
| 職務基礎資料 | `generateJobBasic()` | [index.html:2330](index.html#L2330) | `onclick="generateJobBasic()"` |
| 部門職責維護 | `generateDeptFunction()` | [index.html:3778](index.html#L3778) | `onclick="generateDeptFunction()"` |
| 崗位描述 (JD) | `generateJobDesc()` | [index.html:2425](index.html#L2425) | `onclick="generateJobDesc()"` |
---
## 頁籤 1: 崗位基礎資料維護
### 📍 函式位置
- **檔案**: `index.html`
- **行數**: 2179-2251
- **函式**: `async function generatePositionBasic()`
### 📝 完整 Prompt
```
請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
[如果有已填寫的資料,會附加此段]
已填寫的資料(請參考這些內容來生成相關的資料):
{JSON格式的已填寫資料}
請「只生成」以下這些尚未填寫的欄位:[動態欄位列表]
欄位說明:
- positionCode: 崗位編號(格式如 ENG-001, MGR-002, SAL-003
- positionName: 崗位名稱
- positionCategory: 崗位類別代碼01=技術職, 02=管理職, 03=業務職, 04=行政職)
- positionNature: 崗位性質代碼FT=全職, PT=兼職, CT=約聘, IN=實習)
- headcount: 編制人數1-10之間的數字字串
- positionLevel: 崗位級別L1到L7
- positionDesc: 崗位描述2-3句話描述工作內容
- positionRemark: 崗位備注(可選的補充說明)
請直接返回JSON格式只包含需要生成的欄位不要有任何其他文字
{
"positionCode": "...",
"positionName": "...",
...
}
```
### 🎯 Prompt 設計依據
1. **上下文感知**: 如果使用者已填寫部分欄位,這些資料會被傳入作為參考
2. **精準指令**: 明確告知只生成「尚未填寫」的欄位,避免覆蓋已有資料
3. **格式規範**: 提供詳細的欄位格式說明和代碼對照表
4. **結構化輸出**: 要求返回純 JSON方便程式解析
### 📦 處理的欄位
```javascript
const allFields = [
'positionCode', // 崗位編號
'positionName', // 崗位名稱
'positionCategory', // 崗位類別代碼
'positionNature', // 崗位性質代碼
'headcount', // 編制人數
'positionLevel', // 崗位級別
'positionDesc', // 崗位描述
'positionRemark' // 崗位備注
];
```
### 🔧 如何修改此 Prompt
在 [index.html:2205-2223](index.html#L2205) 找到以下程式碼:
```javascript
const prompt = `請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
${contextInfo}
請「只生成」以下這些尚未填寫的欄位:${emptyFields.join(', ')}
欄位說明:
- positionCode: 崗位編號(格式如 ENG-001, MGR-002, SAL-003
...
`;
```
**修改建議**:
- 如果要改變生成風格:修改第一句的指令(例如:「請以專業正式的語氣生成...」)
- 如果要新增欄位規則:在「欄位說明」中添加新的規範
- 如果要調整格式:修改格式範例(如改變編號規則)
---
## 頁籤 2: 崗位招聘要求
### 📍 函式位置
- **檔案**: `index.html`
- **行數**: 2253-2328
- **函式**: `async function generatePositionRecruit()`
### 📝 完整 Prompt
```
請為HR崗位管理系統生成「{崗位名稱}」的招聘要求資料。請用繁體中文回覆。
已填寫的資料(請參考這些內容來生成相關的資料):
{JSON格式的已填寫資料包含 positionName}
請「只生成」以下這些尚未填寫的欄位:[動態欄位列表]
欄位說明:
- minEducation: 最低學歷代碼HS=高中職, JC=專科, BA=大學, MA=碩士, PHD=博士)
- requiredGender: 要求性別(空字串=不限, M=男, F=女)
- salaryRange: 薪酬范圍代碼A=30000以下, B=30000-50000, C=50000-80000, D=80000-120000, E=120000以上, N=面議)
- workExperience: 工作經驗年數0=不限, 1, 3, 5, 10
- minAge: 最小年齡18-30之間的數字字串
- maxAge: 最大年齡35-55之間的數字字串
- jobType: 工作性質代碼FT=全職, PT=兼職, CT=約聘, DP=派遣)
- recruitPosition: 招聘職位代碼ENG=工程師, MGR=經理, AST=助理, OP=作業員, SAL=業務)
- jobTitle: 職位名稱
- jobDesc: 職位描述2-3句話
- positionReq: 崗位要求(條列式,用換行分隔)
- skillReq: 技能要求(用逗號分隔)
- langReq: 語言要求
- otherReq: 其他要求
請直接返回JSON格式只包含需要生成的欄位
{
"minEducation": "...",
...
}
```
### 🎯 Prompt 設計依據
1. **職位名稱作為核心上下文**: 使用第一個頁籤的崗位名稱作為生成依據
2. **跨頁籤資料引用**: 會從「崗位基礎資料」頁籤讀取 `positionName`
3. **招聘專用代碼**: 提供完整的學歷、薪資、經驗等代碼對照
### 📦 處理的欄位
```javascript
const allFields = [
'minEducation', // 最低學歷
'requiredGender', // 要求性別
'salaryRange', // 薪酬范圍
'workExperience', // 工作經驗年數
'minAge', // 最小年齡
'maxAge', // 最大年齡
'jobType', // 工作性質
'recruitPosition', // 招聘職位
'jobTitle', // 職位名稱
'jobDesc', // 職位描述
'positionReq', // 崗位要求
'skillReq', // 技能要求
'langReq', // 語言要求
'otherReq' // 其他要求
];
```
### 🔧 如何修改此 Prompt
在 [index.html:2275-2301](index.html#L2275) 找到程式碼。
**修改建議**:
- **調整薪資範圍**: 修改 `salaryRange` 的代碼對照(例如增加更高薪資級別)
- **新增性別選項**: 如果需要更多性別選項,在 `requiredGender` 中添加
- **調整經驗年限**: 修改 `workExperience` 的可用選項
---
## 頁籤 3: 職務基礎資料
### 📍 函式位置
- **檔案**: `index.html`
- **行數**: 2330-2423
- **函式**: `async function generateJobBasic()`
### 📝 完整 Prompt
```
請為HR職務管理系統生成職務基礎資料。請用繁體中文回覆。
[如果有已填寫的資料,會附加此段]
已填寫的資料(請參考這些內容來生成相關的資料):
{JSON格式的已填寫資料}
請「只生成」以下這些尚未填寫的欄位:[動態欄位列表,可能包含 checkbox]
欄位說明:
- jobCategoryCode: 職務類別代碼MGR=管理職, TECH=技術職, SALE=業務職, ADMIN=行政職, RD=研發職, PROD=生產職)
- jobCode: 職務編號(格式如 MGR-001, TECH-002
- jobName: 職務名稱
- jobNameEn: 職務英文名稱
- jobHeadcount: 編制人數1-20之間的數字字串
- jobSortOrder: 排列順序10, 20, 30...的數字字串)
- jobRemark: 備注說明
- jobLevel: 職務層級(可以是 *保密* 或具體層級)
- hasAttendanceBonus: 是否有全勤true/false
- hasHousingAllowance: 是否住房補貼true/false
請直接返回JSON格式只包含需要生成的欄位
{
"jobCategoryCode": "...",
...
}
```
### 🎯 Prompt 設計依據
1. **職務 vs 崗位**: 這個頁籤處理的是「職務」Job與「崗位」Position不同
2. **Checkbox 處理**: 特殊處理 `hasAttendanceBonus``hasHousingAllowance` 兩個布林值欄位
3. **排序欄位**: `jobSortOrder` 使用 10 的倍數,方便後續插入新職務
### 📦 處理的欄位
```javascript
const allFields = [
'jobCategoryCode', // 職務類別代碼
'jobCode', // 職務編號
'jobName', // 職務名稱
'jobNameEn', // 職務英文名稱
'jobHeadcount', // 編制人數
'jobSortOrder', // 排列順序
'jobRemark', // 備注說明
'jobLevel' // 職務層級
];
// 額外處理的 checkbox
const checkboxes = [
'hasAttendanceBonus', // 是否有全勤
'hasHousingAllowance' // 是否住房補貼
];
```
### 🔧 如何修改此 Prompt
在 [index.html:2362-2382](index.html#L2362) 找到程式碼。
**修改建議**:
- **新增職務類別**: 在 `jobCategoryCode` 中添加新的類別代碼
- **調整編號格式**: 修改 `jobCode` 的格式範例
- **修改保密設定**: 調整 `jobLevel` 的說明(如不允許保密)
---
## 頁籤 4: 部門職責維護
### 📍 函式位置
- **檔案**: `index.html`
- **行數**: 3778-3839
- **函式**: `function generateDeptFunction()`
### 📝 完整 Prompt
```
請為HR部門職責管理系統生成部門職責資料。請用繁體中文回覆。
[如果有已填寫的資料,會附加此段]
已填寫的資料(請參考這些內容來生成相關的資料):
{JSON格式的已填寫資料}
請「只生成」以下這些尚未填寫的欄位:[動態欄位列表]
欄位說明:
- 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格式只包含需要生成的欄位不要有任何其他文字
{
"deptFunctionCode": "...",
...
}
```
### 🎯 Prompt 設計依據
1. **條列式格式**: 特別要求使用 `•` 開頭的條列式,符合部門職責文件慣例
2. **數量控制**: 明確指定每個欄位的條列項目數量(例如使命 2-3 項)
3. **事業體代碼**: 提供固定的事業體代碼選項
4. **管理導向**: 專注於部門管理層面的使命、願景、職責、KPI
### 📦 處理的欄位
```javascript
const allFields = [
'deptFunctionCode', // 部門職責編號
'deptFunctionName', // 部門職責名稱
'deptFunctionBU', // 事業體代碼
'deptFunctionDept', // 部門名稱
'deptManager', // 部門主管職稱
'deptMission', // 部門使命(條列式)
'deptVision', // 部門願景(條列式)
'deptCoreFunctions', // 核心職責(條列式)
'deptKPIs' // 關鍵績效指標(條列式)
];
```
### 🔧 如何修改此 Prompt
在 [index.html:3800-3819](index.html#L3800) 找到程式碼。
**修改建議**:
- **新增事業體**: 在 `deptFunctionBU` 中添加新的事業體代碼
- **調整條列數量**: 修改各欄位的條列項目數量要求
- **改變條列符號**: 將 `•` 改為其他符號(如 `1.`, `-`, `★`
---
## 頁籤 5: 崗位描述 (JD)
### 📍 函式位置
- **檔案**: `index.html`
- **行數**: 2425-2541
- **函式**: `async function generateJobDesc()`
### 📝 完整 Prompt
```
請為HR崗位描述管理系統生成崗位描述資料。請用繁體中文回覆。
[如果有已填寫的資料,會附加此段]
已填寫的資料(請參考這些內容來生成相關的資料):
{JSON格式的已填寫資料欄位名稱已移除 jd_ 前綴}
請「只生成」以下這些尚未填寫的欄位:[動態欄位列表]
欄位說明:
- empNo: 工號(格式如 A001234
- empName: 員工姓名
- positionCode: 崗位代碼
- versionDate: 版本日期YYYY-MM-DD格式
- positionName: 崗位名稱
- department: 所屬部門
- positionEffectiveDate: 崗位生效日期YYYY-MM-DD格式
- directSupervisor: 直接領導職務
- directReports: 直接下級(格式如「工程師 x 5人」
- workLocation: 任職地點代碼HQ=總部, TPE=台北, TYC=桃園, KHH=高雄, SH=上海, SZ=深圳)
- empAttribute: 員工屬性代碼FT=正式員工, CT=約聘, PT=兼職, IN=實習, DP=派遣)
- positionPurpose: 崗位設置目的1句話說明
- mainResponsibilities: 主要崗位職責用「1、」「2、」「3、」「4、」「5、」格式每項換行用\n分隔
- education: 教育程度要求
- basicSkills: 基本技能要求
- professionalKnowledge: 專業知識要求
- workExperienceReq: 工作經驗要求
- otherRequirements: 其他要求
請直接返回JSON格式只包含需要生成的欄位
{
"empNo": "...",
...
}
```
### 🎯 Prompt 設計依據
1. **最複雜的表單**: 包含最多欄位18 個),涵蓋完整的 JD 內容
2. **日期格式規範**: 明確要求 YYYY-MM-DD 格式
3. **職責編號格式**: 特別指定使用「1、」「2、」格式並用 `\n` 換行
4. **地點代碼對照**: 提供台灣與中國大陸的辦公室代碼
5. **欄位名稱映射**: 程式中會將 `jd_` 前綴移除後再傳給 API
### 📦 處理的欄位
```javascript
const allFields = [
'jd_empNo', // 工號
'jd_empName', // 員工姓名
'jd_positionCode', // 崗位代碼
'jd_versionDate', // 版本日期
'jd_positionName', // 崗位名稱
'jd_department', // 所屬部門
'jd_positionEffectiveDate', // 崗位生效日期
'jd_directSupervisor', // 直接領導職務
'jd_directReports', // 直接下級
'jd_workLocation', // 任職地點
'jd_empAttribute', // 員工屬性
'jd_positionPurpose', // 崗位設置目的
'jd_mainResponsibilities', // 主要崗位職責
'jd_education', // 教育程度要求
'jd_basicSkills', // 基本技能要求
'jd_professionalKnowledge', // 專業知識要求
'jd_workExperienceReq', // 工作經驗要求
'jd_otherRequirements' // 其他要求
];
```
### 🔧 如何修改此 Prompt
在 [index.html:2464-2492](index.html#L2464) 找到程式碼。
**特別注意**: 這個模組有欄位名稱映射機制([index.html:2499-2518](index.html#L2499)),修改欄位時需要同時更新 `fieldMapping` 物件。
**修改建議**:
- **新增辦公室地點**: 在 `workLocation` 中添加新的辦公室代碼
- **調整職責數量**: 修改 `mainResponsibilities` 的編號範圍(如改為 1-10
- **新增員工屬性**: 在 `empAttribute` 中添加新的員工類型
---
## 如何修改 Prompt
### 📝 通用修改步驟
所有 "I'm Feeling Lucky" 按鈕的 Prompt 都遵循相同的修改流程:
#### 步驟 1: 找到對應函式
使用上方表格找到要修改的函式位置,例如:
```
崗位基礎資料維護 → index.html:2179
```
#### 步驟 2: 找到 prompt 變數
在函式中搜尋 `const prompt =``const prompt = \``:
```javascript
const prompt = `請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
...
`;
```
#### 步驟 3: 修改 Prompt 內容
根據需求修改:
**A. 修改生成風格**
```javascript
// 修改前
const prompt = `請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
// 修改後(加入語氣要求)
const prompt = `請以專業正式且友善的語氣為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
```
**B. 修改欄位說明**
```javascript
// 修改前
- positionCode: 崗位編號(格式如 ENG-001, MGR-002, SAL-003
// 修改後(改變編號規則)
- positionCode: 崗位編號(格式:部門縮寫-年份-流水號,如 ENG-2024-001
```
**C. 新增欄位規則**
```javascript
// 在「欄位說明」區塊新增
- positionPriority: 優先級HIGH=高, MID=中, LOW=低)
```
**D. 調整輸出格式**
```javascript
// 修改前
請直接返回JSON格式只包含需要生成的欄位不要有任何其他文字
// 修改後(要求更多資訊)
請直接返回JSON格式只包含需要生成的欄位。每個欄位請加上「_note」後綴提供生成理由
{
"positionCode": "ENG-001",
"positionCode_note": "根據技術職的慣例生成",
...
}
```
#### 步驟 4: 測試修改結果
1. 儲存 `index.html` 檔案
2. 重新整理瀏覽器頁面Ctrl+F5 強制重新整理)
3. 點擊對應頁籤的 "✨ I'm feeling lucky" 按鈕
4. 檢查生成的內容是否符合預期
### ⚠️ 修改時的注意事項
1. **保持 JSON 格式要求**: 必須要求 LLM 返回純 JSON否則程式解析會失敗
2. **不要移除欄位動態列表**: `${emptyFields.join(', ')}` 這段必須保留
3. **維持上下文機制**: `${contextInfo}` 這段是自動填入已有資料的機制,不要刪除
4. **注意反引號**: Prompt 使用反引號 `` ` `` 包裹,內部不可再使用反引號
5. **測試跨欄位引用**: 有些頁籤會引用其他頁籤的資料(如招聘要求引用崗位名稱)
---
## Prompt 設計原則
### 🎨 系統採用的 Prompt 設計原則
#### 1. **智能空白檢測**
```javascript
const emptyFields = getEmptyFields(allFields);
```
- 只生成尚未填寫的欄位
- 避免覆蓋使用者已輸入的資料
- 提升生成效率
#### 2. **上下文感知生成**
```javascript
const contextInfo = Object.keys(existingData).length > 0
? `\n\n已填寫的資料請參考這些內容來生成相關的資料\n${JSON.stringify(existingData, null, 2)}`
: '';
```
- 將已填寫的資料作為上下文
- 讓 LLM 生成與現有資料一致的內容
- 提升資料連貫性
#### 3. **結構化輸出**
```javascript
const prompt = `...
請直接返回JSON格式只包含需要生成的欄位不要有任何其他文字
{
${emptyFields.map(f => `"${f}": "..."`).join(',\n ')}
}`;
```
- 要求 LLM 返回純 JSON
- 提供 JSON 格式範本
- 方便程式解析
#### 4. **詳細的欄位說明**
```javascript
欄位說明:
- positionCode: 崗位編號(格式如 ENG-001, MGR-002, SAL-003
- positionCategory: 崗位類別代碼01=技術職, 02=管理職, 03=業務職, 04=行政職)
```
- 提供完整的代碼對照表
- 說明格式規範和範例
- 減少生成錯誤
#### 5. **繁體中文優先**
```javascript
const prompt = `請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
```
- 明確要求使用繁體中文
- 避免出現簡體字或英文
### 🚀 進階 Prompt 技巧
#### 技巧 1: 加入企業特色
```javascript
// 在 prompt 開頭加入
const prompt = `你是一位專業的HR顧問熟悉台灣半導體產業的人力資源管理。
請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
```
#### 技巧 2: 加入範例
```javascript
// 在欄位說明後加入
生成範例
{
"positionCode": "ENG-001",
"positionName": "前端工程師",
"positionDesc": "負責開發和維護公司網站前端功能,提升使用者體驗。"
}
請參考以上範例生成類似格式的資料
```
#### 技巧 3: 加入條件邏輯
```javascript
// 根據職位類型調整 prompt
const isManagerPosition = existingData.positionCategory === '02';
const extraInstruction = isManagerPosition
? '\n特別注意這是管理職請強調領導能力和團隊管理經驗。'
: '';
const prompt = `請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。${extraInstruction}
```
#### 技巧 4: 加入驗證規則
```javascript
const prompt = `...
請確保生成的資料符合以下規則:
1. 崗位編號必須以部門代碼開頭
2. 編制人數必須為正整數
3. 崗位描述長度在 20-100 字之間
4. 所有代碼必須從提供的選項中選擇
...`;
```
---
## 🔍 偵錯與問題排查
### 常見問題 1: LLM 返回格式錯誤
**現象**: 點擊按鈕後出現「生成失敗,請稍後再試」
**可能原因**:
- LLM 返回的不是純 JSON 格式
- JSON 中包含多餘的文字說明
- JSON 格式不正確(缺少逗號、括號等)
**解決方法**:
在 Prompt 中加強格式要求:
```javascript
const prompt = `...
請直接返回JSON格式不要包含任何markdown標記如 \`\`\`json不要有任何其他文字說明
{
${emptyFields.map(f => `"${f}": "..."`).join(',\n ')}
}`;
```
### 常見問題 2: 生成內容不符合預期
**現象**: 生成的內容格式正確,但內容不理想
**解決方法**:
1. 檢查上下文資料是否正確傳遞
2. 增加更詳細的欄位說明
3. 提供具體範例
4. 調整 Prompt 語氣和指令
### 常見問題 3: 部分欄位未填充
**現象**: 只填充了部分欄位,其他欄位仍為空
**可能原因**:
- LLM 返回的 JSON 缺少某些欄位
- 欄位名稱不匹配(大小寫、前綴問題)
**解決方法**:
檢查欄位映射邏輯,特別是 `generateJobDesc()` 函式中的 `fieldMapping`
```javascript
const fieldMapping = {
'empNo': 'jd_empNo',
'empName': 'jd_empName',
...
};
```
---
## 📚 參考資源
### 相關檔案
- **主程式**: [index.html](index.html) - 包含所有 AI 生成函式
- **LLM API 配置**: [llm_config.py](llm_config.py) - LLM 模型設定
- **環境變數**: [.env](.env) - API Key 和模型設定
### 相關函式
- **`callClaudeAPI(prompt)`**: 呼叫 LLM API 的核心函式
- **`getEmptyFields(allFields)`**: 檢測空白欄位
- **`fillIfEmpty(fieldId, value)`**: 只填充空白欄位
- **`setButtonLoading(btn, isLoading)`**: 設定按鈕載入狀態
### LLM 模型設定
系統支援多種 LLM 模型,在 `.env` 檔案中設定:
```env
# Gemini API Configuration
GEMINI_API_KEY=your_api_key
GEMINI_MODEL=gemini-1.5-flash
# DeepSeek API Configuration
DEEPSEEK_API_KEY=your_deepseek_api_key
DEEPSEEK_API_URL=https://api.deepseek.com/v1
# OpenAI API Configuration
OPENAI_API_KEY=your_openai_api_key
OPENAI_API_URL=https://api.openai.com/v1
# Ollama API Configuration
OLLAMA_API_URL=https://ollama_pjapi.theaken.com
OLLAMA_MODEL=deepseek-reasoner
```
---
## ✅ 最佳實踐
### ✨ 撰寫好的 Prompt 的建議
1. **明確的指令**: 清楚說明要生成什麼類型的資料
2. **詳細的格式說明**: 提供完整的代碼對照表和格式範例
3. **上下文資訊**: 包含已填寫的資料作為參考
4. **結構化輸出**: 要求返回 JSON 格式,方便解析
5. **語言偏好**: 明確指定使用繁體中文
6. **錯誤處理**: 加入驗證規則,減少生成錯誤
### 📝 Prompt 維護建議
1. **版本控制**: 重大修改前先備份原 Prompt
2. **測試驗證**: 每次修改後都要測試所有情境
3. **文件更新**: 修改後同步更新此說明文件
4. **使用者回饋**: 根據實際使用情況調整 Prompt
---
## 🎯 總結
### 快速參考表
| 頁籤 | 函式 | 程式碼行數 | 主要用途 | 欄位數量 |
|-----|------|-----------|---------|---------|
| 崗位基礎資料 | `generatePositionBasic()` | 2179-2251 | 生成崗位基本資訊 | 8 |
| 崗位招聘要求 | `generatePositionRecruit()` | 2253-2328 | 生成招聘需求 | 14 |
| 職務基礎資料 | `generateJobBasic()` | 2330-2423 | 生成職務資訊 | 8+2 checkbox |
| 部門職責維護 | `generateDeptFunction()` | 3778-3839 | 生成部門職責 | 9 |
| 崗位描述 (JD) | `generateJobDesc()` | 2425-2541 | 生成完整JD | 18 |
### 核心機制
1. **智能檢測**: 自動識別空白欄位
2. **上下文感知**: 參考已填寫資料生成
3. **結構化輸出**: JSON 格式便於解析
4. **安全填充**: 只填充空白欄位,不覆蓋現有資料
---
> **維護提醒**: 當系統新增或修改欄位時,記得同步更新:
> 1. HTML 表單欄位
> 2. JavaScript 函式中的 `allFields` 陣列
> 3. Prompt 中的欄位說明
> 4. 此說明文件
---
**文件結束** | 有問題請找 AI不要找我 ¯\\_(ツ)_/¯

263
docs/三個錦囊設計.md Normal file
View File

@@ -0,0 +1,263 @@
# 三個錦囊功能設計
## 功能概述
將原本的單一 "I'm feeling lucky" 按鈕改為三個可自定義的 AI 生成按鈕("三個錦囊")。
## UI 設計
### 視覺效果
```
┌─────────────────────────────────────────────────────────┐
│ 🎁 錦囊一 🎁 錦囊二 🎁 錦囊三 │
│ [標題] [標題] [標題] │
│ ⚙️ 編輯 ⚙️ 編輯 ⚙️ 編輯 │
└─────────────────────────────────────────────────────────┘
```
### 互動流程
1. **點擊錦囊按鈕** → 使用預設或自定義的 prompt 調用 AI
2. **點擊編輯圖示** → 彈出對話框,顯示並可編輯當前 prompt
3. **Prompt 儲存** → 儲存至 localStorage按模組區分
## 資料結構
### LocalStorage 結構
```javascript
{
"prompts": {
"positionBasic": {
"bag1": { "title": "簡化版", "prompt": "..." },
"bag2": { "title": "標準版", "prompt": "..." },
"bag3": { "title": "詳細版", "prompt": "..." }
},
"positionRecruit": {
"bag1": { "title": "一般需求", "prompt": "..." },
"bag2": { "title": "高階需求", "prompt": "..." },
"bag3": { "title": "專業需求", "prompt": "..." }
},
"jobBasic": { ... },
"deptFunction": { ... },
"jobDesc": { ... }
}
}
```
## 預設 Prompt 模板
### 崗位基礎資料 - 基礎資料頁籤
#### 錦囊一:簡化版(僅填必填欄位)
```
你是專業人資顧問,熟悉半導體製造業。請生成崗位基礎資料(僅必填欄位)。
已填寫的資料:{existingData}
需要生成的欄位positionCode, positionName
請用繁體中文,返回 JSON 格式。
```
#### 錦囊二:標準版(常用欄位)
```
你是專業人資顧問,熟悉半導體製造業。請生成崗位基礎資料(標準版)。
已填寫的資料:{existingData}
需要生成的欄位positionCode, positionName, positionCategory, positionLevel, headcount
欄位說明:
- positionCode: 崗位編號(格式如 ENG-001
- positionName: 崗位名稱
- positionCategory: 崗位類別代碼01=技術職, 02=管理職, 03=業務職, 04=行政職)
- positionLevel: 崗位級別L1-L7
- headcount: 編制人數1-10
請用繁體中文,返回 JSON 格式。
```
#### 錦囊三:詳細版(所有欄位)
```
你是專業人資顧問,熟悉半導體製造業的人資所有流程。請生成完整的崗位基礎資料。
已填寫的資料:{existingData}
需要生成的欄位positionCode, positionName, positionCategory, positionNature, headcount, positionLevel, positionDesc, positionRemark
欄位說明:
- positionCode: 崗位編號(格式如 ENG-001, MGR-002
- positionName: 崗位名稱
- positionCategory: 崗位類別代碼01=技術職, 02=管理職, 03=業務職, 04=行政職)
- positionNature: 崗位性質代碼FT=全職, PT=兼職, CT=約聘, IN=實習)
- headcount: 編制人數1-10之間的數字字串
- positionLevel: 崗位級別L1到L7
- positionDesc: 崗位描述(條列式,用換行分隔)
- positionRemark: 崗位備注(條列式,用換行分隔)
請用繁體中文,返回 JSON 格式,不要有任何其他文字。
```
### 崗位基礎資料 - 招聘要求頁籤
#### 錦囊一:基本需求
```
請生成「{positionName}」的基本招聘要求。
已填寫的資料:{existingData}
需要生成的欄位minEducation, workExperience, jobType, jobTitle
請用繁體中文,返回 JSON 格式。
```
#### 錦囊二:標準需求
```
請生成「{positionName}」的標準招聘要求。
已填寫的資料:{existingData}
需要生成的欄位minEducation, salaryRange, workExperience, jobType, recruitPosition, jobTitle, positionReq, skillReq
欄位說明:
- minEducation: 最低學歷代碼HS=高中職, JC=專科, BA=大學, MA=碩士, PHD=博士)
- salaryRange: 薪酬范圍代碼A=30000以下, B=30000-50000, C=50000-80000, D=80000-120000, E=120000以上, N=面議)
- workExperience: 工作經驗年數0=不限, 1, 3, 5, 10
- jobType: 工作性質代碼FT=全職, PT=兼職, CT=約聘, DP=派遣)
- recruitPosition: 招聘職位代碼ENG=工程師, MGR=經理, AST=助理, OP=作業員, SAL=業務)
- positionReq: 崗位要求(條列式,用換行分隔)
- skillReq: 技能要求(條列式,用換行分隔)
請用繁體中文,返回 JSON 格式。
```
#### 錦囊三:完整需求
```
請生成「{positionName}」的完整招聘要求資料。
已填寫的資料:{existingData}
需要生成所有空白欄位。
(完整欄位說明同標準版,包含所有 18 個欄位)
請用繁體中文,返回 JSON 格式。
```
## 實作步驟
### 1. HTML 結構更新
將單一按鈕:
```html
<button type="button" class="ai-generate-btn" onclick="generatePositionBasic()">
<span>✨ I'm feeling lucky</span>
</button>
```
改為三個錦囊:
```html
<div class="ai-bags-container">
<div class="ai-bag" data-bag="1" onclick="executeAIBag('positionBasic', 1)">
<div class="bag-icon">🎁</div>
<div class="bag-title" id="bag1-title-positionBasic">錦囊一</div>
<button class="bag-edit-btn" onclick="editBagPrompt(event, 'positionBasic', 1)">⚙️</button>
</div>
<div class="ai-bag" data-bag="2" onclick="executeAIBag('positionBasic', 2)">
<div class="bag-icon">🎁</div>
<div class="bag-title" id="bag2-title-positionBasic">錦囊二</div>
<button class="bag-edit-btn" onclick="editBagPrompt(event, 'positionBasic', 2)">⚙️</button>
</div>
<div class="ai-bag" data-bag="3" onclick="executeAIBag('positionBasic', 3)">
<div class="bag-icon">🎁</div>
<div class="bag-title" id="bag3-title-positionBasic">錦囊三</div>
<button class="bag-edit-btn" onclick="editBagPrompt(event, 'positionBasic', 3)">⚙️</button>
</div>
</div>
```
### 2. CSS 樣式
```css
.ai-bags-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.ai-bag {
position: relative;
padding: 20px;
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(155, 89, 182, 0.3);
}
.ai-bag:hover {
transform: translateY(-4px);
box-shadow: 0 6px 20px rgba(155, 89, 182, 0.5);
}
.ai-bag .bag-icon {
font-size: 2rem;
text-align: center;
margin-bottom: 8px;
}
.ai-bag .bag-title {
color: white;
font-weight: 600;
text-align: center;
font-size: 0.95rem;
}
.ai-bag .bag-edit-btn {
position: absolute;
top: 8px;
right: 8px;
background: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s ease;
}
.ai-bag .bag-edit-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
```
### 3. JavaScript 函式
#### 執行 AI 錦囊
```javascript
async function executeAIBag(module, bagNumber) {
const prompts = getModulePrompts(module);
const bagPrompt = prompts[`bag${bagNumber}`];
// 使用 prompt 調用 AI
await callClaudeAPI(bagPrompt.prompt);
}
```
#### 編輯 Prompt
```javascript
function editBagPrompt(event, module, bagNumber) {
event.stopPropagation();
// 顯示編輯對話框
showPromptEditModal(module, bagNumber);
}
```
## 優點
1.**靈活性高** - 用戶可自定義每個錦囊的用途
2.**效率提升** - 三個預設選項涵蓋不同需求場景
3.**學習友好** - 顯示 prompt 有助於理解 AI 運作
4.**可擴展** - 未來可增加更多錦囊或分享功能
## 待實作功能
- [ ] HTML 結構更新5 個模組)
- [ ] CSS 樣式新增
- [ ] JavaScript 函式實作
- [ ] LocalStorage 管理
- [ ] 編輯 Modal 對話框
- [ ] 預設 Prompt 初始化

View File

@@ -0,0 +1,26 @@
project_name/
├── .env # 環境變數(不進版控)
├── .env.example # 環境變數範本
├── .gitignore
├── README.md
├── requirements.txt
├── app.py # 主程式入口
├── config.py # 設定檔
├── preview.html # UI 預覽
├── docs/
│ ├── SDD.md # 系統設計文件
│ ├── security_audit.md # 資安檢視報告
│ ├── user_command_log.md # 用戶指令記錄
│ ├── CHANGELOG.md # 版本變更紀錄
│ └── API_DOC.md # API 文件
├── models/ # 資料庫模型
├── routes/ # 路由模組
├── services/ # 商業邏輯
├── utils/ # 工具函式
├── templates/ # HTML 模板
└── static/ # 靜態資源
├── css/
├── js/
└── images/

176
docs/更新欄位名稱.md Normal file
View File

@@ -0,0 +1,176 @@
# 系統表單欄位規範書 (Standardized Field Specifications)
## 1. 命名規範與前綴定義 (Naming Conventions)
為了確保系統一致性HTML 元素 ID 採用 `[模組前綴]_[標準欄位名]` 的命名方式。
| 模組名稱 | 模組前綴 (Prefix) | 說明 |
| :--- | :--- | :--- |
| **崗位管理 (Position)** | `pos_` | 崗位基礎資料 |
| **招聘條件 (Recruit)** | `rec_` | 崗位內的招聘頁籤 |
| **職務管理 (Job)** | `job_` | 全公司通用的職務定義 |
| **部門職責 (DeptFunc)** | `df_` | 部門功能與職責定義 |
| **崗位描述 (JobDesc)** | `jd_` | 最終的 JD 產出表單 |
---
## 2. 崗位基礎資料模組 (Position Module)
**表單代號**: `positionForm`
**資料表**: `Position`
### 2.1 基礎資料頁籤 (tab-position-basic)
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 預設 | 備註 |
|---|---|---|---|---|---|---|---|
| 1 | 事業體 | `pos_businessUnit` | `businessUnit` | select | 否 | - | SBU, MBU... (聯動L1) |
| 2 | 處級單位 | `pos_division` | `division` | select | 否 | - | (聯動L2) |
| 3 | 部級單位 | `pos_department` | `department` | select | 否 | - | (聯動L3) |
| 4 | 課級單位 | `pos_section` | `section` | text | 否 | - | - |
| 5 | **崗位編號** | `pos_code` | `positionCode` | text | **是** | - | 唯一識別碼 (PK) |
| 6 | 生效日期 | `pos_effectiveDate` | `effectiveDate` | date | 否 | Today | - |
| 7 | **崗位名稱** | `pos_name` | `positionName` | text | **是** | - | - |
| 8 | 崗位級別 | `pos_level` | `positionLevel` | select | 否 | - | L1-L7 |
| 9 | 崗位類別 | `pos_category` | `positionCategory` | select | 否 | - | onchange 觸發 |
| 10 | 崗位類別名稱 | `pos_categoryName` | `positionCategoryName` | text | 否 | - | readonly |
| 11 | 崗位性質 | `pos_type` | `positionType` | select | 否 | - | FT, PT, CT, IN |
| 12 | 崗位性質名稱 | `pos_typeName` | `positionTypeName` | text | 否 | - | readonly |
| 13 | 編制人數 | `pos_headcount` | `headcount` | number | 否 | 0 | min=0 |
| 14 | 崗位描述 | `pos_desc` | `description` | textarea | 否 | - | rows=6 |
| 15 | 崗位備注 | `pos_remark` | `remark` | textarea | 否 | - | rows=6 |
### 2.2 招聘要求資料頁籤 (tab-position-recruit)
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 預設 | 備註 |
|---|---|---|---|---|---|---|---|
| 1 | 最低學歷 | `rec_eduLevel` | `educationLevel` | select | 否 | - | HS, BA, MA, PHD |
| 2 | 要求性別 | `rec_gender` | `requiredGender` | select | 否 | Any | M, F, Any |
| 3 | 薪酬范圍 | `rec_salaryRange` | `salaryRange` | select | 否 | - | A-E, Negotiable |
| 4 | 工作經驗 | `rec_expYears` | `experienceYears` | select | 否 | - | 0, 1, 3, 5, 10+ |
| 5 | 最小年齡 | `rec_minAge` | `minAge` | number | 否 | - | min=18 |
| 6 | 最大年齡 | `rec_maxAge` | `maxAge` | number | 否 | - | max=65 |
| 7 | 工作性質 | `rec_jobType` | `jobType` | select | 否 | - | 招聘用性質分類 |
| 8 | 招聘職位 | `rec_position` | `recruitPosition` | select | 否 | - | ENG, MGR... |
| 9 | 職位名稱(對外) | `rec_jobTitle` | `jobTitle` | text | 否 | - | 對外招聘用Title |
| 10 | 上級崗位編號 | `rec_superiorCode` | `superiorPositionCode` | text | 否 | - | - |
| 11 | 職位描述(JD) | `rec_jobDesc` | `recruitJobDesc` | textarea | 否 | - | 招聘廣告用 |
| 12 | 崗位要求(Req) | `rec_positionReq` | `recruitRequirements` | textarea | 否 | - | 招聘廣告用 |
| 13 | 證照要求 | `rec_certReq` | `certRequirements` | select | 否 | - | - |
| 14 | 專業要求 | `rec_majorReq` | `majorRequirements` | text | 否 | - | Modal選擇 |
| 15 | 技能要求 | `rec_skillReq` | `skillRequirements` | text | 否 | - | Tags input |
| 16 | 語言要求 | `rec_langReq` | `langRequirements` | text | 否 | - | - |
| 17 | 其他要求 | `rec_otherReq` | `otherRequirements` | text | 否 | - | - |
| 18 | 招聘備注 | `rec_remark` | `recruitRemark` | textarea | 否 | - | - |
---
## 3. 職務基礎資料模組 (Job Module)
**表單代號**: `jobForm`
**資料表**: `Job`
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 預設 | 備註 |
|---|---|---|---|---|---|---|---|
| 1 | **職務類別編號** | `job_category` | `jobCategoryCode` | select | **是** | - | onchange 觸發 |
| 2 | 職務類別名稱 | `job_categoryName` | `jobCategoryName` | text | 否 | - | readonly |
| 3 | **職務編號** | `job_code` | `jobCode` | text | **是** | - | 唯一識別碼 |
| 4 | **職務名稱** | `job_name` | `jobName` | text | **是** | - | - |
| 5 | 職務英文 | `job_nameEn` | `jobNameEn` | text | 否 | - | - |
| 6 | 生效日期 | `job_effectiveDate`| `effectiveDate` | date | 否 | - | - |
| 7 | 職務層級 | `job_level` | `jobLevel` | text | 否 | *保密* | **敏感欄位** |
| 8 | 編制人數 | `job_headcount` | `headcount` | number | 否 | - | - |
| 9 | 排列順序 | `job_sortOrder` | `sortOrder` | number | 否 | - | - |
| 10 | 全勤獎金 | `job_hasAttBonus` | `hasAttendanceBonus` | checkbox| 否 | false| Toggle Switch |
| 11 | 住房補貼 | `job_hasHouseAllow`| `hasHousingAllowance` | checkbox| 否 | false| Toggle Switch |
| 12 | 職務備注 | `job_remark` | `remark` | textarea | 否 | - | - |
---
## 4. 部門職責模組 (DeptFunction Module)
**表單代號**: `deptFunctionForm`
**資料表**: `DeptFunction`
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 預設 | 備註 |
|---|---|---|---|---|---|---|---|
| 1 | **職責編號** | `df_code` | `dfCode` | text | **是** | - | DF-001 |
| 2 | **職責名稱** | `df_name` | `dfName` | text | **是** | - | - |
| 3 | **事業體** | `df_businessUnit` | `businessUnit` | select | **是** | - | (已合併重複欄位) |
| 4 | **處級單位** | `df_division` | `division` | select | **是** | - | - |
| 5 | **部級單位** | `df_department` | `department` | select | **是** | - | - |
| 6 | 課級單位 | `df_section` | `section` | text | 否 | - | - |
| 7 | **對應崗位** | `df_posTitle` | `positionTitle` | select | **是** | - | 關聯 Position |
| 8 | 崗位級別 | `df_posLevel` | `positionLevel` | select | 否 | - | 自動帶出或指定 |
| 9 | 部門主管職稱 | `df_managerTitle` | `managerTitle` | text | 否 | - | - |
| 10 | **生效日期** | `df_effectiveDate` | `effectiveDate` | date | **是** | - | - |
| 11 | 人數上限 | `df_headcountLimit`| `headcountLimit` | number | 否 | - | - |
| 12 | 狀態 | `df_status` | `status` | select | 否 | active | - |
| 13 | 部門使命 | `df_mission` | `mission` | textarea | 否 | - | - |
| 14 | 部門願景 | `df_vision` | `vision` | textarea | 否 | - | - |
| 15 | **核心職責** | `df_coreFunc` | `coreFunctions` | textarea | **是** | - | - |
| 16 | KPIs | `df_kpis` | `kpis` | textarea | 否 | - | - |
| 17 | 協作部門 | `df_collab` | `collaboration` | textarea | 否 | - | - |
| 18 | 備注 | `df_remark` | `remark` | textarea | 否 | - | - |
---
## 5. 崗位描述模組 (JobDescription Module)
**表單代號**: `jobDescForm`
**資料表**: `JobDescription` (部分欄位為 View)
### 5.1 基本信息 (Header)
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|---|---|---|---|---|---|---|
| 1 | 工號 | `jd_empNo` | `empNo` | text | 否 | Search Modal |
| 2 | 姓名 | `jd_empName` | `empName` | text | 否 | Readonly |
| 3 | 崗位代碼 | `jd_posCode` | `positionCode` | text | 否 | 關聯鍵 |
| 4 | 版本日期 | `jd_versionDate` | `versionDate` | date | 否 | - |
### 5.2 崗位資訊 (Position Info - Readonly/Derived)
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|---|---|---|---|---|---|---|
| 1 | 崗位名稱 | `jd_posName` | `positionName` | text | 否 | - |
| 2 | 事業體 | `jd_businessUnit` | `businessUnit` | select | 否 | - |
| 3 | 處級單位 | `jd_division` | `division` | select | 否 | - |
| 4 | 部級單位 | `jd_department` | `department` | select | 否 | - |
| 5 | 課級單位 | `jd_section` | `section` | text | 否 | - |
| 6 | 崗位級別 | `jd_posLevel` | `positionLevel` | select | 否 | - |
| 7 | 生效日期 | `jd_posEffDate` | `positionEffectiveDate`| date | 否 | - |
| 8 | **直接主管** | `jd_supervisor` | `directSupervisor` | text | 否 | - |
| 9 | 職等&職務 | `jd_gradeJob` | `positionGradeJob` | text | 否 | Modal |
| 10 | **匯報對象** | `jd_reportTo` | `reportTo` | text | 否 | Modal |
| 11 | 直接下屬 | `jd_directReports` | `directReports` | text | 否 | - |
| 12 | 任職地點 | `jd_location` | `workLocation` | select | 否 | - |
| 13 | 員工屬性 | `jd_empAttr` | `empAttribute` | select | 否 | - |
### 5.3 職責與要求 (Details)
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|---|---|---|---|---|---|---|
| 1 | 部門職責代碼 | `jd_dfCode` | `dfCode` | text | 否 | 關聯 DeptFunction |
| 2 | 崗位設置目的 | `jd_purpose` | `positionPurpose` | text | 否 | - |
| 3 | **主要職責** | `jd_mainResp` | `mainResponsibilities`| textarea | 否 | 編號清單 |
| 4 | 教育程度 | `jd_eduLevel` | `educationLevel` | text | 否 | - |
| 5 | 基本技能 | `jd_basicSkills` | `basicSkills` | textarea | 否 | - |
| 6 | 專業知識 | `jd_proKnowledge` | `professionalKnowledge` | textarea | 否 | - |
| 7 | 工作經驗 | `jd_expReq` | `experienceRequirements`| textarea | 否 | - |
| 8 | 其他要求 | `jd_otherReq` | `otherRequirements` | textarea | 否 | - |
---
## 6. 共用設定與資料字典
### 6.1 模態框 (Modals)
* `MajorModal` (專業要求)
* `EmpSearchModal` (員工搜索)
* `OrgSearchModal` (組織搜索)
* `GradeJobModal` (職等職務)
* `ReportToModal` (匯報對象)
### 6.2 特殊控件
* **Toggle Switch**: 用於所有布林值 (Boolean) 欄位。
* **Numbered Textarea**: 用於 `mainResponsibilities`,自動產生序號。
* **Cascading Selects**: 組織層級 (BU -> Div -> Dept -> Section) 統一使用標準聯動邏輯。

236
docs/權限矩陣.md Normal file
View File

@@ -0,0 +1,236 @@
# 系統權限矩陣 - 那都AI寫的不要問我
## 系統概述
本系統為「人力資源崗位管理系統」(HR Position Management System),採用三級權限架構設計,確保資料安全性與操作權限的合理分配。
---
## 角色定義
### 1. 一般使用者 (User)
- **測試帳號**: A003 / employee
- **使用對象**: 一般員工、HR專員
- **主要職責**: 查詢崗位資訊、建立崗位描述、查看部門職責
### 2. 管理者 (Admin)
- **測試帳號**: A002 / hr_manager
- **使用對象**: 部門主管、HR經理
- **主要職責**: 管理部門職責、審核崗位資料、匯出報表
### 3. 最高管理者 (Super Admin)
- **測試帳號**: A001 / admin
- **使用對象**: 系統管理員、HR總監
- **主要職責**: 系統設定、使用者管理、完整權限控制
---
## 功能權限矩陣
| 功能模組 | 功能項目 | 一般使用者 | 管理者 | 最高管理者 | 說明 |
|---------|---------|:---------:|:-----:|:---------:|------|
| **崗位管理** | 查看崗位清單 | ✅ | ✅ | ✅ | 所有角色可查看 |
| | 搜尋/篩選崗位 | ✅ | ✅ | ✅ | 所有角色可搜尋 |
| | 查看崗位詳情 | ✅ | ✅ | ✅ | 所有角色可查看詳情 |
| | 建立新崗位 | ❌ | ✅ | ✅ | 需要管理權限 |
| | 編輯崗位資訊 | ❌ | ✅ | ✅ | 需要管理權限 |
| | 刪除崗位 | ❌ | ❌ | ✅ | 僅最高管理者 |
| **職位描述 (JD)** | 查看 JD | ✅ | ✅ | ✅ | 所有角色可查看 |
| | 建立 JD | ✅ | ✅ | ✅ | 所有角色可建立 |
| | 編輯自己的 JD | ✅ | ✅ | ✅ | 可編輯自己建立的 |
| | 編輯所有 JD | ❌ | ✅ | ✅ | 管理者以上 |
| | 刪除 JD | ❌ | ✅ | ✅ | 管理者以上 |
| | 使用 AI 生成 JD | ✅ | ✅ | ✅ | 所有角色可使用 AI |
| **部門職責** | 查看部門職責 | ✅ | ✅ | ✅ | 所有角色可查看 |
| | 建立部門職責 | ❌ | ✅ | ✅ | 管理者以上 |
| | 編輯部門職責 | ❌ | ✅ | ✅ | 管理者以上 |
| | 刪除部門職責 | ❌ | ❌ | ✅ | 僅最高管理者 |
| | 匯出部門職責 | ✅ | ✅ | ✅ | 所有角色可匯出 |
| **崗位清單** | 查看清單 | ✅ | ✅ | ✅ | 所有角色可查看 |
| | 篩選/排序 | ✅ | ✅ | ✅ | 所有角色可使用 |
| | 匯出 CSV | ✅ | ✅ | ✅ | 所有角色可匯出 |
| | 批量操作 | ❌ | ✅ | ✅ | 管理者以上 |
| **報表匯出** | 匯出基本報表 | ✅ | ✅ | ✅ | 所有角色可匯出 |
| | 匯出完整資料 | ❌ | ✅ | ✅ | 管理者以上 |
| | 匯出統計報表 | ❌ | ✅ | ✅ | 管理者以上 |
| **系統管理** | 查看系統設定 | ❌ | ❌ | ✅ | 僅最高管理者 |
| | 修改系統設定 | ❌ | ❌ | ✅ | 僅最高管理者 |
| | LLM 模型設定 | ❌ | ❌ | ✅ | 僅最高管理者 |
| | 測試 API 連線 | ❌ | ❌ | ✅ | 僅最高管理者 |
| **使用者管理** | 查看使用者清單 | ❌ | ❌ | ✅ | 僅最高管理者 |
| | 新增使用者 | ❌ | ❌ | ✅ | 僅最高管理者 |
| | 編輯使用者 | ❌ | ❌ | ✅ | 僅最高管理者 |
| | 刪除使用者 | ❌ | ❌ | ✅ | 僅最高管理者 |
| | 修改權限 | ❌ | ❌ | ✅ | 僅最高管理者 |
| **AI 功能** | 使用 AI 生成 | ✅ | ✅ | ✅ | 所有角色可使用 |
| | 選擇 AI 模型 | ❌ | ❌ | ✅ | 僅最高管理者設定 |
| | 查看 AI 使用記錄 | ❌ | ✅ | ✅ | 管理者以上 |
---
## 資料訪問權限
### 資料可見性
| 資料類型 | 一般使用者 | 管理者 | 最高管理者 |
|---------|:---------:|:-----:|:---------:|
| 所有崗位資料 | ✅ 唯讀 | ✅ 可編輯 | ✅ 完全控制 |
| 部門職責資料 | ✅ 唯讀 | ✅ 可編輯 | ✅ 完全控制 |
| 自己建立的 JD | ✅ 可編輯 | ✅ 可編輯 | ✅ 可編輯 |
| 他人建立的 JD | ✅ 唯讀 | ✅ 可編輯 | ✅ 可編輯 |
| 使用者資料 | ❌ | ❌ | ✅ 完全控制 |
| 系統設定 | ❌ | ❌ | ✅ 完全控制 |
| 操作日誌 | ❌ | ✅ 唯讀 | ✅ 完全控制 |
### 資料操作權限
| 操作類型 | 一般使用者 | 管理者 | 最高管理者 |
|---------|:---------:|:-----:|:---------:|
| **C**reate (新增) | 僅 JD | 崗位、部門職責、JD | 所有資料 |
| **R**ead (讀取) | 基本資料 | 包含統計資料 | 所有資料 |
| **U**pdate (更新) | 僅自己的 JD | 大部分資料 | 所有資料 |
| **D**elete (刪除) | ❌ | 部分資料 | 所有資料 |
---
## 頁面/模組訪問權限
| 頁面模組 | 一般使用者 | 管理者 | 最高管理者 |
|---------|:---------:|:-----:|:---------:|
| 🏠 首頁 (登入頁) | ✅ | ✅ | ✅ |
| 📝 崗位說明書管理 | ✅ | ✅ | ✅ |
| 🎯 部門職責管理 | ✅ 唯讀 | ✅ | ✅ |
| 📋 崗位清單 | ✅ | ✅ | ✅ |
| ⚙️ 管理者頁面 | ❌ | ⚠️ 部分功能 | ✅ |
### 管理者頁面功能細分
| 管理者頁面功能 | 一般使用者 | 管理者 | 最高管理者 |
|--------------|:---------:|:-----:|:---------:|
| 使用者管理 | ❌ | ❌ | ✅ |
| LLM 模型設定 | ❌ | ❌ | ✅ |
| 崗位資料管理 | ❌ | ✅ 唯讀 | ✅ |
| 匯出完整資料 | ❌ | ✅ | ✅ |
| 查看統計資訊 | ❌ | ✅ | ✅ |
---
## 特殊權限說明
### 1. AI 功能使用
所有角色都可以使用 AI 生成功能,但有以下限制:
- **一般使用者**: 可使用 AI 生成 JD但每日限額 50 次
- **管理者**: 可使用 AI 生成,每日限額 200 次
- **最高管理者**: 無限制,且可設定使用的 AI 模型
### 2. 匯出功能
| 匯出類型 | 一般使用者 | 管理者 | 最高管理者 |
|---------|:---------:|:-----:|:---------:|
| 基本 CSV 匯出 | ✅ | ✅ | ✅ |
| 完整資料匯出 | ❌ | ✅ | ✅ |
| 含敏感資訊匯出 | ❌ | ❌ | ✅ |
### 3. 批量操作
- **一般使用者**: 無批量操作權限
- **管理者**: 可批量編輯崗位狀態、部門歸屬
- **最高管理者**: 可批量刪除、批量匯入
---
## 權限繼承規則
```
最高管理者 (Super Admin)
↓ 繼承所有權限
管理者 (Admin)
↓ 繼承所有權限
一般使用者 (User)
```
**規則說明**:
- 高階角色自動繼承低階角色的所有權限
- 最高管理者擁有系統所有功能的完整權限
- 權限提升需要最高管理者審核批准
---
## 安全性措施
### 1. 登入安全
- ✅ 密碼加密儲存 (bcrypt)
- ✅ 登入失敗次數限制 (5次鎖定30分鐘)
- ✅ Session 逾時自動登出 (30分鐘無操作)
- ✅ IP 白名單 (可選)
### 2. 操作追蹤
- ✅ 所有資料修改記錄操作者
- ✅ 關鍵操作留存日誌 (刪除、權限變更)
- ✅ 管理者以上角色操作全程記錄
### 3. 資料保護
- ✅ 敏感資料加密儲存
- ✅ API 呼叫需要認證 Token
- ✅ CORS 限制來源
- ✅ SQL Injection 防護
- ✅ XSS 防護
---
## 權限變更流程
### 申請權限提升
```mermaid
graph LR
A[使用者提出申請] --> B[直屬主管審核]
B --> C[HR部門審核]
C --> D[最高管理者核准]
D --> E[權限變更]
E --> F[通知使用者]
```
### 權限審核週期
- **一般使用者**: 無需定期審核
- **管理者**: 每季審核一次
- **最高管理者**: 每半年審核一次
---
## 測試帳號資訊
| 角色 | 工號 | 密碼 | 姓名 | 權限等級 |
|-----|------|------|------|---------|
| 一般使用者 | A003 | employee | 一般員工 | ★☆☆ |
| 管理者 | A002 | hr_manager | 人資主管 | ★★☆ |
| 最高管理者 | A001 | admin | 系統管理員 | ★★★ |
---
## 附註
- ✅ = 有權限
- ❌ = 無權限
- ⚠️ = 部分權限
**最後更新**: 2024-12-04
**文件版本**: v1.0
**維護者**: AI (所以有問題不要問我)
---
## 權限擴充建議
未來可考慮新增以下角色:
1. **部門管理者**: 僅能管理自己部門的崗位
2. **唯讀管理者**: 可查看所有資料但無編輯權限
3. **稽核員**: 專門查看操作日誌和系統使用情況
4. **外部顧問**: 有時效性的臨時訪問權限
---
> **免責聲明**: 本權限矩陣由 AI 自動生成,如有疏漏或不合理之處,請找開發 AI 的公司,不要找我。¯\\\_(ツ)\_/¯

347
docs/表單欄位清單.md Normal file
View File

@@ -0,0 +1,347 @@
# 表單欄位完整清單
## 1. 崗位基礎資料模組 - 基礎資料頁籤 (positionForm - tab-position-basic)
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|---|-----------|---------|------------|------|------|------|------|
| 1 | 事業體 (Business Unit) | businessUnit | businessUnit | select | 否 | 空值 | SBU, MBU, HQBU, ITBU, HRBU, ACCBU |
| 2 | 處級單位 (Division) | division | division | select | 否 | 空值 | 根據事業體變動 |
| 3 | 部級單位 (Department) | department | department | select | 否 | 空值 | 根據處級單位變動 |
| 4 | 課級單位 (Section) | section | section | text | 否 | 選填 | - |
| 5 | 崗位編號 * | positionCode | positionCode | text | **是** | 空值 | 唯一識別碼,可更改 |
| 6 | 生效日期 | effectiveDate | effectiveDate | date | 否 | 2001-01-01 | - |
| 7 | 崗位名稱 * | positionName | positionName | text | **是** | 空值 | - |
| 8 | 崗位級別 | positionLevel | positionLevel | select | 否 | 空值 | L1-L7 (基層至總經理) |
| 9 | 崗位類別 | positionCategory | positionCategory | select | 否 | 空值 | 01, 02, 03, 04有onchange事件 |
| 10 | 崗位類別名稱 | positionCategoryName | positionCategoryName | text | 否 | 自動帶出 | readonly |
| 11 | 崗位性質 | positionNature | positionNature | select | 否 | 空值 | FT(全職), PT(兼職), CT(約聘), IN(實習)有onchange事件 |
| 12 | 崗位性質名稱 | positionNatureName | positionNatureName | text | 否 | 自動帶出 | readonly |
| 13 | 編制人數 | headcount | headcount | number | 否 | 空值 | min=0 |
| 14 | 崗位描述(條列式說明) | positionDesc | positionDesc | textarea | 否 | 空值 | rows=6有範例提示 |
| 15 | 崗位備注(條列式說明) | positionRemark | positionRemark | textarea | 否 | 空值 | rows=6有範例提示 |
---
## 2. 崗位基礎資料模組 - 招聘要求資料頁籤 (positionForm - tab-position-recruit)
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|---|-----------|---------|------------|------|------|------|------|
| 1 | 最低學歷 | minEducation | minEducation | select | 否 | 空值 | HS, JC, BA, MA, PHD |
| 2 | 要求性別 | requiredGender | requiredGender | select | 否 | 不限 | M(男), F(女) |
| 3 | 薪酬范圍 | salaryRange | salaryRange | select | 否 | 空值 | A-E, N(面議) |
| 4 | 工作經驗 | workExperience | workExperience | select | 否 | 空值 | 0, 1, 3, 5, 10 (年) |
| 5 | 最小年齡 | minAge | minAge | number | 否 | 空值 | min=18, max=65 |
| 6 | 最大年齡 | maxAge | maxAge | number | 否 | 空值 | min=18, max=65 |
| 7 | 工作性質 | jobType | jobType | select | 否 | 空值 | FT, PT, CT, DP |
| 8 | 職位名稱 | jobTitle | jobTitle | text | 否 | 空值 | - |
| 9 | 招聘職位 | recruitPosition | recruitPosition | select | 否 | 空值 | ENG, MGR, AST, OP, SAL |
| 10 | 上級崗位編號 | superiorPosition | superiorPosition | text | 否 | 空值 | - |
| 11 | 職位描述 | jobDesc | jobDesc | textarea | 否 | 空值 | rows=3 |
| 12 | 崗位要求 | positionReq | positionReq | textarea | 否 | 空值 | rows=3 |
| 13 | 職稱要求 | titleReq | titleReq | select | 否 | 空值 | NONE, CERT, LIC |
| 14 | 專業要求 | majorReq | majorReq | text | 否 | 點擊選擇 | readonly有modal選擇器 |
| 15 | 技能要求 | skillReq | skillReq | text | 否 | 空值 | 例Excel, Python, SAP... |
| 16 | 語言要求 | langReq | langReq | text | 否 | 空值 | 例英文中級、日文N2... |
| 17 | 其他要求 | otherReq | otherReq | text | 否 | 空值 | - |
| 18 | 備注說明 | recruitRemark | recruitRemark | textarea | 否 | 空值 | rows=4 |
---
## 3. 職務基礎資料模組 (jobForm - tab-job-basic)
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|---|-----------|---------|------------|------|------|------|------|
| 1 | 職務類別編號 * | jobCategoryCode | jobCategoryCode | select | **是** | 空值 | MGR, TECH, SALE, ADMIN, RD, PROD有onchange事件 |
| 2 | 職務類別名稱 | jobCategoryName | jobCategoryName | text | 否 | 自動帶出 | readonly |
| 3 | 職務編號 * | jobCode | jobCode | text | **是** | 空值 | 可更改職務編號 |
| 4 | 職務名稱 * | jobName | jobName | text | **是** | 空值 | - |
| 5 | 職務英文 | jobNameEn | jobNameEn | text | 否 | 空值 | - |
| 6 | 生效日期 | jobEffectiveDate | jobEffectiveDate | date | 否 | 空值 | - |
| 7 | 編制人數 | jobHeadcount | jobHeadcount | number | 否 | 空值 | min=0 |
| 8 | 排列順序 | jobSortOrder | jobSortOrder | number | 否 | 空值 | min=0 |
| 9 | 備注說明 | jobRemark | jobRemark | textarea | 否 | 空值 | rows=4 |
| 10 | 職務層級 | jobLevel | jobLevel | text | 否 | 如:*保密* | 敏感信息欄位 |
| 11 | 是否有全勤 | hasAttendanceBonus | hasAttendanceBonus | checkbox | 否 | 否 | toggle-switch 控件 |
| 12 | 是否住房補貼 | hasHousingAllowance | hasHousingAllowance | checkbox | 否 | 否 | toggle-switch 控件 |
---
## 4. 部門職責模組 (deptFunctionForm)
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|---|-----------|---------|------------|------|------|------|------|
| 1 | 部門職責編號 * | deptFunctionCode | deptFunctionCode | text | **是** | 空值 | 例如: DF-001 |
| 2 | 部門職責名稱 * | deptFunctionName | deptFunctionName | text | **是** | 空值 | 例如: 軟體研發部職責 |
| 3 | 事業體 (Business Unit) * (第1個) | deptFunctionBU | deptFunctionBU | select | **是** | 空值 | SBU, MBU, HQBU, ITBU, HRBU, ACCBU |
| 4 | 事業體 (Business Unit) * (第2個) | deptFunc_businessUnit | businessUnit | select | **是** | 空值 | 聯動選擇 |
| 5 | 處級單位 (Division) * | deptFunc_division | division | select | **是** | 空值 | 聯動選擇 |
| 6 | 部級單位 (Department) * | deptFunc_department | department | select | **是** | 空值 | 聯動選擇 |
| 7 | 課級單位 (Section) | deptFunc_section | section | text | 否 | 選填 | - |
| 8 | 崗位名稱 * | deptFunc_positionTitle | positionTitle | select | **是** | 空值 | 根據部級單位變動 |
| 9 | 崗位級別 | deptFunc_positionLevel | positionLevel | select | 否 | 空值 | E, M, S, D, VP, C |
| 10 | 部門主管職稱 | deptManager | deptManager | text | 否 | 例如: 部門經理 | - |
| 11 | 生效日期 * | deptFunctionEffectiveDate | deptFunctionEffectiveDate | date | **是** | 空值 | - |
| 12 | 部門人數上限 | deptHeadcount | deptHeadcount | number | 否 | 例如: 50 | min=1 |
| 13 | 部門狀態 | deptStatus | deptStatus | select | 否 | active | active, inactive, planning |
| 14 | 部門使命 (Mission) | deptMission | deptMission | textarea | 否 | 空值 | rows=3有範例提示 |
| 15 | 部門願景 (Vision) | deptVision | deptVision | textarea | 否 | 空值 | rows=3有範例提示 |
| 16 | 核心職責 (Core Functions) * | deptCoreFunctions | deptCoreFunctions | textarea | **是** | 空值 | rows=6有範例提示 |
| 17 | 關鍵績效指標 (KPIs) | deptKPIs | deptKPIs | textarea | 否 | 空值 | rows=4有範例提示 |
| 18 | 協作部門 | deptCollaboration | deptCollaboration | textarea | 否 | 空值 | rows=3有範例提示 |
| 19 | 備注 | deptFunctionRemark | deptFunctionRemark | textarea | 否 | 空值 | rows=3 |
---
## 5. 崗位描述模組 (jobDescForm - tab-jobdesc-basic)
### 5.1 基本信息區塊
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|---|-----------|---------|------------|------|------|------|------|
| 1 | 工號 | jd_empNo | empNo | text | 否 | 空值 | 有員工搜索modal |
| 2 | 姓名 | jd_empName | empName | text | 否 | 自動帶出 | readonly |
| 3 | 崗位代碼 | jd_positionCode | positionCode | text | 否 | 空值 | - |
| 4 | 版本更新日期 | jd_versionDate | versionDate | date | 否 | 空值 | - |
### 5.2 崗位基本信息區塊
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|---|-----------|---------|------------|------|------|------|------|
| 1 | 崗位名稱 | jd_positionName | positionName | text | 否 | 空值 | - |
| 2 | 事業體 (Business Unit) | jd_businessUnit | businessUnit | select | 否 | 空值 | 聯動選擇 |
| 3 | 處級單位 (Division) | jd_division | division | select | 否 | 空值 | 聯動選擇 |
| 4 | 部級單位 (Department) | jd_department | department | select | 否 | 空值 | 聯動選擇 |
| 5 | 課級單位 (Section) | jd_section | section | text | 否 | 選填 | - |
| 6 | 崗位名稱 (重複) | jd_positionTitle | positionTitle | select | 否 | 空值 | 根據部級單位變動 |
| 7 | 崗位級別 | jd_positionLevel | positionLevel | select | 否 | 空值 | E, M, S, D, VP, C |
| 8 | 崗位生效日期 | jd_positionEffectiveDate | positionEffectiveDate | date | 否 | 空值 | - |
| 9 | 直接領導職務 | jd_directSupervisor | directSupervisor | text | 否 | 空值 | - |
| 10 | 崗位職等&職務 | jd_positionGradeJob | positionGradeJob | text | 否 | 點擊選擇 | readonly有modal選擇器 |
| 11 | 匯報對象職務 | jd_reportTo | reportTo | text | 否 | 點擊選擇 | readonly有modal選擇器 |
| 12 | 直接下級(職位及人數) | jd_directReports | directReports | text | 否 | 例:工程師 x 5人 | - |
| 13 | 任職地點 | jd_workLocation | workLocation | select | 否 | 空值 | HQ, TPE, TYC, KHH, SH, SZ |
| 14 | 員工屬性 | jd_empAttribute | empAttribute | select | 否 | 空值 | FT, CT, PT, IN, DP |
### 5.3 部門職責資訊區塊 (自動帶入,隱藏顯示)
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|---|-----------|---------|------------|------|------|------|------|
| 1 | 部門職責編號 | jd_deptFunctionCode | deptFunctionCode | text | 否 | 自動帶出 | readonlyid="deptFunctionInfoSection" |
| 2 | 事業體 | jd_deptFunctionBU | deptFunctionBU | text | 否 | 自動帶出 | readonly |
| 3 | 部門使命 | jd_deptMission | deptMission | textarea | 否 | 自動帶出 | readonlyrows=2 |
| 4 | 部門核心職責 | jd_deptCoreFunctions | deptCoreFunctions | textarea | 否 | 自動帶出 | readonlyrows=4 |
| 5 | 部門 KPIs | jd_deptKPIs | deptKPIs | textarea | 否 | 自動帶出 | readonlyrows=3 |
### 5.4 職責描述區塊
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|---|-----------|---------|------------|------|------|------|------|
| 1 | 崗位設置目的 | jd_positionPurpose | positionPurpose | text | 否 | 空值 | 有展開編輯按鈕 |
| 2 | 主要崗位職責 | jd_mainResponsibilities | mainResponsibilities | textarea | 否 | 空值 | rows=8numbered-textarea有數字編號 |
### 5.5 崗位要求區塊
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|---|-----------|---------|------------|------|------|------|------|
| 1 | 教育程度 | jd_education | education | text | 否 | 例:大學本科及以上 | - |
| 2 | 基本技能 | jd_basicSkills | basicSkills | textarea | 否 | 空值 | rows=2有展開編輯按鈕 |
| 3 | 專業知識 | jd_professionalKnowledge | professionalKnowledge | textarea | 否 | 空值 | rows=2有展開編輯按鈕 |
| 4 | 工作經驗 | jd_workExperienceReq | workExperienceReq | textarea | 否 | 空值 | rows=2有展開編輯按鈕 |
| 5 | 其他 | jd_otherRequirements | otherRequirements | textarea | 否 | 空值 | rows=3 |
---
## 6. 崗位清單模組 (positionListTable)
### 6.1 表格列(只讀顯示)
| # | 列標題 | 資料鍵值 | 資料類型 | 可排序 | 備註 |
|---|------|--------|------|------|------|
| 1 | 崗位編號 | positionCode | text | 是 | 可點擊排序 |
| 2 | 崗位名稱 | positionName | text | 是 | 可點擊排序 |
| 3 | 崗位類別 | positionCategory | text | 是 | 可點擊排序 |
| 4 | 崗位性質 | positionNature | text | 是 | 可點擊排序 |
| 5 | 編制人數 | headcount | number | 是 | 可點擊排序 |
| 6 | 崗位等級 | positionLevel | text | 是 | 可點擊排序 |
| 7 | 生效日期 | effectiveDate | date | 是 | 可點擊排序 |
| 8 | 操作 | - | button | 否 | 編輯/刪除按鈕 |
---
## 數據結構對應關係
### Position (崗位基礎資料)
```javascript
{
id: string,
basicInfo: {
positionCode: string,
positionName: string,
positionCategory: string,
positionCategoryName: string,
positionNature: string,
positionNatureName: string,
headcount: number,
positionLevel: string,
effectiveDate: date,
positionDesc: string,
positionRemark: string
},
recruitInfo: {
minEducation: string,
requiredGender: string,
salaryRange: string,
workExperience: number,
minAge: number,
maxAge: number,
jobType: string,
recruitPosition: string,
jobTitle: string,
jobDesc: string,
positionReq: string,
titleReq: string,
majorReq: string,
skillReq: string,
langReq: string,
otherReq: string,
superiorPosition: string,
recruitRemark: string
},
createdAt: datetime,
updatedAt: datetime
}
```
### Job (職務基礎資料)
```javascript
{
id: string,
jobCategoryCode: string,
jobCategoryName: string,
jobCode: string,
jobName: string,
jobNameEn: string,
jobEffectiveDate: date,
jobHeadcount: number,
jobSortOrder: number,
jobRemark: string,
jobLevel: string,
hasAttendanceBonus: boolean,
hasHousingAllowance: boolean,
createdAt: datetime,
updatedAt: datetime
}
```
### DeptFunction (部門職責)
```javascript
{
deptFunctionCode: string,
deptFunctionName: string,
deptFunctionBU: string,
businessUnit: string,
division: string,
department: string,
section: string,
positionTitle: string,
positionLevel: string,
deptManager: string,
deptFunctionEffectiveDate: date,
deptHeadcount: number,
deptStatus: string,
deptMission: string,
deptVision: string,
deptCoreFunctions: string,
deptKPIs: string,
deptCollaboration: string,
deptFunctionRemark: string
}
```
### JobDescription (崗位描述)
```javascript
{
basicInfo: {
empNo: string,
empName: string,
positionCode: string,
versionDate: date
},
positionInfo: {
positionName: string,
businessUnit: string,
division: string,
department: string,
section: string,
positionTitle: string,
positionLevel: string,
positionEffectiveDate: date,
directSupervisor: string,
positionGradeJob: string,
reportTo: string,
directReports: string,
workLocation: string,
empAttribute: string,
deptFunctionCode: string,
deptFunctionBU: string,
deptMission: string,
deptCoreFunctions: string,
deptKPIs: string
},
responsibilities: {
positionPurpose: string,
mainResponsibilities: string
},
requirements: {
education: string,
basicSkills: string,
professionalKnowledge: string,
workExperienceReq: string,
otherRequirements: string
}
}
```
---
## 關鍵特性總結
### 聯動選擇Cascading Select
- **事業體 → 處級單位 → 部級單位 → 課級單位**
- 在崗位基礎資料、部門職責、崗位描述中都有此聯動
- 觸發事件onchange="onXXXBusinessUnitChange/onXXXDivisionChange" 等
### 自動帶出欄位 (readonly)
- 崗位類別名稱 (根據崗位類別自動帶出)
- 崗位性質名稱 (根據崗位性質自動帶出)
- 職務類別名稱 (根據職務類別編號自動帶出)
- 員工姓名 (根據工號自動帶出)
- 部門職責相關資訊 (自動帶入到崗位描述)
### Modal/模態框
- 專業要求選擇 (Major Modal - majorModal)
- 員工搜索 (EmpSearchModal)
- 組織搜索 (OrgSearchModal)
- 職等職務選擇 (GradeJobModal)
- 匯報對象選擇 (ReportToModal)
### CSV 操作
- 職務基礎資料支援: 下載範本、匯出、匯入
- 部門職責支援: 匯入、匯出
### 特殊欄位
- **敏感信息欄位**: jobLevel (職務層級) - 標記為 *保密*
- **布林值(Checkbox)**: hasAttendanceBonus, hasHousingAllowance
- **Toggle Switch**: 用於布林值的友善UI
### 表單驗證
- 必填欄位用 `<span class="required">*</span>` 標記
- HTML5 required 屬性用於部分欄位
---
## 變更紀錄
| 日期 | 版本 | 變更內容 |
|------|------|--------|
| 2025-12-05 | v1.0 | 初版完整表單欄位清單 |

265
docs/需求確認文件.md Normal file
View File

@@ -0,0 +1,265 @@
# HR 基礎資料維護系統 - 需求確認文件
**文件版本**1.0
**建立日期**2024-12-06
**最後更新**2024-12-06
**文件狀態**:待確認
---
## 1. 專案基本資訊
| 項目 | 內容 |
|------|------|
| 專案名稱 | HR 基礎資料維護系統 |
| 專案目的 | 提供人力資源部門維護崗位、職務、部門職責等基礎資料的工具 |
| 目標用戶 | HR 人員、部門主管、系統管理員 |
| 開發狀態 | 開發中 |
---
## 2. 功能模組確認
### 2.1 崗位基礎資料模組
| 功能項目 | 狀態 | 備註 |
|----------|------|------|
| 新增崗位 | 已實現 | |
| 編輯崗位 | 已實現 | |
| 刪除崗位 | 已實現 | |
| 崗位編號變更 | 已實現 | |
| 基礎資料頁籤 | 已實現 | 15 個欄位 |
| 招聘要求頁籤 | 已實現 | 18 個欄位 |
| 組織層級聯動 | 已實現 | BU → 處 → 部 → 課 |
**確認問題**
- [ ] 崗位編號格式是否需要強制規範?(目前接受任意格式)
- [ ] 是否需要崗位編號自動生成功能?
- [ ] 編制人數是否有上限限制?
---
### 2.2 職務基礎資料模組
| 功能項目 | 狀態 | 備註 |
|----------|------|------|
| 新增職務 | 已實現 | |
| 編輯職務 | 已實現 | |
| 刪除職務 | 已實現 | |
| 職務類別聯動 | 已實現 | 6 種類別 |
| Toggle 開關 | 已實現 | 全勤獎金、住房補貼 |
| 職務層級(保密欄位) | 已實現 | |
**確認問題**
- [ ] 職務類別是否需要擴充?
- [ ] 保密欄位的顯示邏輯是否正確?
- [ ] 是否需要職務與崗位的關聯功能?
---
### 2.3 部門職責模組
| 功能項目 | 狀態 | 備註 |
|----------|------|------|
| 新增部門職責 | 已實現 | |
| 編輯部門職責 | 已實現 | |
| 刪除部門職責 | 已實現 | |
| 組織層級聯動 | 已實現 | |
| 使命願景維護 | 已實現 | |
| 核心職責維護 | 已實現 | |
| KPI 維護 | 已實現 | |
**確認問題**
- [ ] 部門職責是否需要與崗位建立關聯?
- [ ] 是否需要審核流程?
- [ ] 狀態變更是否需要記錄歷史?
---
### 2.4 崗位描述模組
| 功能項目 | 狀態 | 備註 |
|----------|------|------|
| 新增崗位描述 | 已實現 | |
| 編輯崗位描述 | 已實現 | |
| 版本管理 | 部分實現 | 僅記錄版本日期 |
| 主要職責清單 | 已實現 | 編號格式 |
| 崗位要求維護 | 已實現 | |
**確認問題**
- [ ] 是否需要完整版本歷史記錄?
- [ ] 崗位描述是否需要審核流程?
- [ ] 是否需要匯出為 PDF/Word 格式?
---
### 2.5 三個錦囊 AI 功能
| 功能項目 | 狀態 | 備註 |
|----------|------|------|
| 自動補齊 | 已實現 | 根據已填內容生成 |
| 範例模板 | 已實現 | 生成範例資料 |
| 驗證檢查 | 已實現 | 檢查資料完整性 |
| Prompt 編輯 | 已實現 | 可自訂 Prompt |
| 多 LLM 支援 | 已實現 | Ollama, Gemini, DeepSeek, OpenAI |
**確認問題**
- [ ] 預設 Prompt 是否符合需求?
- [ ] 是否需要限制 LLM 使用次數?
- [ ] 是否需要記錄 AI 生成歷史?
---
### 2.6 崗位清單與匯出
| 功能項目 | 狀態 | 備註 |
|----------|------|------|
| 崗位清單顯示 | 已實現 | |
| 搜尋過濾 | 已實現 | |
| 檢視描述 | 已實現 | |
| 匯出 CSV | 已實現 | |
| 下載 CSV 範本 | 已實現 | 崗位、職務 |
| 匯入 CSV | 已實現 | 崗位、職務 |
**確認問題**
- [ ] 是否需要 Excel 格式支援?
- [ ] 匯出時是否需要選擇欄位?
- [ ] 批次匯入的錯誤處理是否符合需求?
---
## 3. 技術規格確認
### 3.1 前端技術
| 項目 | 當前實現 | 確認狀態 |
|------|----------|----------|
| 框架 | 純 HTML5/CSS3/ES6 JavaScript | |
| 模組化 | ES6 Modules (import/export) | |
| 樣式 | CSS Modules | |
| UI 元件 | 自訂元件 | |
**確認問題**
- [ ] 是否考慮使用前端框架Vue/React
- [ ] 是否需要 TypeScript 支援?
### 3.2 後端技術
| 項目 | 當前實現 | 確認狀態 |
|------|----------|----------|
| 框架 | Python Flask | |
| API 風格 | RESTful | |
| 資料庫 | In-Memory (Dict) | |
| LLM 服務 | 多 LLM 支援 | |
**確認問題**
- [ ] 是否需要遷移至正式資料庫MySQL/PostgreSQL
- [ ] 是否需要用戶認證功能?
- [ ] 是否需要與現有 HR 系統整合?
---
## 4. 欄位規格確認
### 4.1 標準化 HTML ID
系統已規劃欄位 ID 標準化,詳見「更新欄位名稱.md」。
| 模組 | 前綴 | 欄位數 | 狀態 |
|------|------|--------|------|
| 崗位基礎資料 | pos_ | 15 | 待實施 |
| 招聘要求 | rec_ | 18 | 待實施 |
| 職務基礎資料 | job_ | 12 | 待實施 |
| 部門職責 | df_ | 18 | 待實施 |
| 崗位描述 | jd_ | 16 | 待實施 |
**確認問題**
- [ ] 是否同意欄位 ID 標準化方案?
- [ ] 重命名是否會影響現有資料?
---
## 5. 使用者介面確認
### 5.1 視覺設計
| 項目 | 當前狀態 | 確認 |
|------|----------|------|
| 主色調 | 藍色系 (#1a5276) | |
| 字型 | Noto Sans TC | |
| 響應式設計 | 支援手機/平板 | |
| 深色模式 | 未實現 | |
**確認問題**
- [ ] 是否需要深色模式?
- [ ] 是否需要自訂主題色?
### 5.2 操作流程
| 流程 | 當前實現 | 確認 |
|------|----------|------|
| 新增 → 保存 | 一步完成 | |
| 保存並新增 | 支援 | |
| 刪除確認 | 彈窗確認 | |
| 快捷鍵 | Ctrl+S, Ctrl+N | |
**確認問題**
- [ ] 是否需要草稿保存功能?
- [ ] 是否需要操作歷史Undo/Redo
---
## 6. 待確認事項清單
### 6.1 優先級高
| # | 事項 | 負責人 | 截止日期 |
|---|------|--------|----------|
| 1 | 確認是否需要用戶認證功能 | | |
| 2 | 確認資料庫選型In-Memory vs MySQL | | |
| 3 | 確認欄位 ID 標準化方案 | | |
| 4 | 確認 AI 功能的使用政策 | | |
### 6.2 優先級中
| # | 事項 | 負責人 | 截止日期 |
|---|------|--------|----------|
| 5 | 確認 Excel 匯出需求 | | |
| 6 | 確認審核流程需求 | | |
| 7 | 確認與現有系統整合需求 | | |
### 6.3 優先級低
| # | 事項 | 負責人 | 截止日期 |
|---|------|--------|----------|
| 8 | 確認深色模式需求 | | |
| 9 | 確認多語言支援需求 | | |
---
## 7. 簽核記錄
| 角色 | 姓名 | 簽核日期 | 簽核狀態 |
|------|------|----------|----------|
| 專案負責人 | | | 待簽核 |
| 業務代表 | | | 待簽核 |
| 技術負責人 | | | 待簽核 |
---
## 8. 附錄
### 附錄 A相關文件
- SDD.md - 軟體設計文件
- TDD.md - 測試設計文件
- 更新欄位名稱.md - 欄位規範書
- ID重命名對照表.md - 欄位 ID 對照表
### 附錄 B系統截圖
(請參考系統實際畫面)
---
**文件結束**

97
hierarchy_test.json Normal file
View File

@@ -0,0 +1,97 @@
{
"stats": {
"business_units": 15,
"divisions": 40,
"departments": 83,
"organization_positions": 237
},
"business_units": [
"半導體事業群",
"汽車事業體",
"法務室",
"岡山製造事業體",
"產品事業體",
"晶圓三廠",
"集團人資行政事業體",
"集團財務事業體",
"集團會計事業體",
"集團資訊事業體",
"新創事業體",
"稽核室",
"總經理室",
"總品質事業體",
"營業事業體"
],
"businessToDivision": {
"半導體事業群": [
"半導體事業群"
],
"汽車事業體": [
"汽車事業體"
],
"法務室": [
"法務室"
],
"岡山製造事業體": [
"生產處",
"岡山製造事業體",
"封裝工程處",
"副總辦公室",
"測試工程與研發處",
"資材處",
"廠務與環安衛管理處"
],
"產品事業體": [
"產品事業體",
"先進產品事業處",
"成熟產品事業處"
],
"晶圓三廠": [
"晶圓三廠",
"製程工程處"
],
"集團人資行政事業體": [
"集團人資行政事業體"
],
"集團財務事業體": [
"集團財務事業體",
"岡山強茂財務處"
],
"集團會計事業體": [
"集團會計事業體",
"岡山會計處",
"集團會計處"
],
"集團資訊事業體": [
"集團資訊事業體",
"資安行動小組",
"資訊一處",
"資訊二處"
],
"新創事業體": [
"新創事業體",
"中低壓產品研發處",
"研發中心",
"高壓產品研發處"
],
"稽核室": [
"稽核室"
],
"總經理室": [
"總經理室",
"ESG專案辦公室",
"專案管理室"
],
"總品質事業體": [
"總品質事業體"
],
"營業事業體": [
"營業事業體",
"商業開發暨市場應用處",
"海外銷售事業處",
"全球技術服務處",
"全球行銷暨業務支援處",
"大中華區銷售事業處"
]
}
}

39
images/logo.svg Normal file
View File

@@ -0,0 +1,39 @@
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
<!-- 背景圓形 -->
<defs>
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
</linearGradient>
<linearGradient id="robotGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#f093fb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#f5576c;stop-opacity:1" />
</linearGradient>
</defs>
<!-- 背景 -->
<circle cx="150" cy="150" r="145" fill="url(#bgGradient)"/>
<!-- AI 機器人頭部 -->
<rect x="90" y="80" width="120" height="100" rx="20" fill="url(#robotGradient)"/>
<!-- 天線 -->
<line x1="150" y1="60" x2="150" y2="80" stroke="#fff" stroke-width="4" stroke-linecap="round"/>
<circle cx="150" cy="55" r="8" fill="#ffd700"/>
<!-- 眼睛 -->
<circle cx="120" cy="120" r="15" fill="#fff"/>
<circle cx="180" cy="120" r="15" fill="#fff"/>
<circle cx="123" cy="123" r="8" fill="#2d3748"/>
<circle cx="183" cy="123" r="8" fill="#2d3748"/>
<!-- 嘴巴 - 顯示為困惑的表情 -->
<path d="M 120 155 Q 150 145 180 155" stroke="#fff" stroke-width="4" fill="none" stroke-linecap="round"/>
<!-- 問號裝飾 -->
<text x="70" y="200" font-family="Arial, sans-serif" font-size="40" fill="#fff" font-weight="bold">?</text>
<text x="210" y="200" font-family="Arial, sans-serif" font-size="40" fill="#fff" font-weight="bold">?</text>
<!-- AI 文字 -->
<text x="150" y="250" font-family="Arial, sans-serif" font-size="32" fill="#fff" font-weight="bold" text-anchor="middle">AI</text>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

349
import_hierarchy_data.py Normal file
View File

@@ -0,0 +1,349 @@
"""
組織階層資料匯入模組
從 Markdown 表格解析組織階層資料,供 Flask API 使用
"""
def import_to_memory():
"""
解析組織階層資料並返回結構化資料
Returns: dict 包含所有階層資料
"""
# 原始資料(從 excel_table copy.md 提取,共 313 筆記錄)
raw_data = [
("半導體事業群", "半導體事業群", "", "營運長"),
("半導體事業群", "半導體事業群", "", "營運長助理"),
("汽車事業體", "汽車事業體", "", "副總經理"),
("汽車事業體", "汽車事業體", "", "專案經理"),
("法務室", "法務室", "", "經副理"),
("法務室", "法務室", "", "法務專員"),
("法務室", "法務室", "", "專利工程師"),
("岡山製造事業體", "生產處", "", "處長"),
("岡山製造事業體", "生產處", "", "專員"),
("岡山製造事業體", "生產處", "生產部", "經副理"),
("岡山製造事業體", "生產處", "生產部", "課長"),
("岡山製造事業體", "生產處", "生產部", "組長"),
("岡山製造事業體", "生產處", "生產部", "班長"),
("岡山製造事業體", "生產處", "生產部", "副班長"),
("岡山製造事業體", "生產處", "生產部", "作業員"),
("岡山製造事業體", "生產處", "生產企劃部", "經副理"),
("岡山製造事業體", "生產處", "生產企劃部", "課長"),
("岡山製造事業體", "生產處", "生產企劃部", "專員"),
("岡山製造事業體", "生產處", "生產企劃部", "工程師"),
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "經副理"),
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "課長"),
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "工程師"),
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "組長"),
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "班長"),
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "副班長"),
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "作業員"),
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "副總經理"),
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "副總經理助理"),
("岡山製造事業體", "封裝工程處", "", "處長"),
("岡山製造事業體", "封裝工程處", "", "專員"),
("岡山製造事業體", "封裝工程處", "", "工程師"),
("岡山製造事業體", "封裝工程處", "製程工程一部", "經副理"),
("岡山製造事業體", "封裝工程處", "製程工程二部", "經副理"),
("岡山製造事業體", "封裝工程處", "製程工程二部", "課長"),
("岡山製造事業體", "封裝工程處", "製程工程二部", "工程師"),
("岡山製造事業體", "封裝工程處", "設備一部", "經副理"),
("岡山製造事業體", "封裝工程處", "設備二部", "經副理"),
("岡山製造事業體", "封裝工程處", "設備二部", "課長"),
("岡山製造事業體", "封裝工程處", "設備二部", "工程師"),
("岡山製造事業體", "副總辦公室", "工業工程部", "經副理"),
("岡山製造事業體", "副總辦公室", "工業工程部", "工程師"),
("岡山製造事業體", "副總辦公室", "工業工程部", "課長"),
("岡山製造事業體", "副總辦公室", "工業工程部", "副理"),
("岡山製造事業體", "測試工程與研發處", "", "處長"),
("岡山製造事業體", "測試工程與研發處", "", "專員"),
("岡山製造事業體", "測試工程與研發處", "測試工程部", "經副理"),
("岡山製造事業體", "測試工程與研發處", "測試工程部", "課長"),
("岡山製造事業體", "測試工程與研發處", "測試工程部", "工程師"),
("岡山製造事業體", "測試工程與研發處", "新產品導入部", "經副理"),
("岡山製造事業體", "測試工程與研發處", "新產品導入部", "專員"),
("岡山製造事業體", "測試工程與研發處", "新產品導入部", "工程師"),
("岡山製造事業體", "測試工程與研發處", "研發部", "經副理"),
("岡山製造事業體", "測試工程與研發處", "研發部", "課長"),
("岡山製造事業體", "測試工程與研發處", "研發部", "工程師"),
("岡山製造事業體", "測試工程與研發處", "研發部", "專員"),
("岡山製造事業體", "資材處", "", "處長"),
("岡山製造事業體", "資材處", "採購部", "經副理"),
("岡山製造事業體", "資材處", "採購部", "課長"),
("岡山製造事業體", "資材處", "採購部", "專員"),
("岡山製造事業體", "資材處", "外部資源部", "專員"),
("岡山製造事業體", "資材處", "生管部", "經副理"),
("岡山製造事業體", "資材處", "生管部", "課長"),
("岡山製造事業體", "資材處", "生管部", "專員"),
("岡山製造事業體", "資材處", "生管部", "班長"),
("岡山製造事業體", "資材處", "生管部", "副班長"),
("岡山製造事業體", "資材處", "生管部", "作業員"),
("岡山製造事業體", "資材處", "原物料控制部", "經副理"),
("岡山製造事業體", "資材處", "原物料控制部", "課長"),
("岡山製造事業體", "資材處", "原物料控制部", "專員"),
("岡山製造事業體", "資材處", "原物料控制部", "班長"),
("岡山製造事業體", "資材處", "原物料控制部", "副班長"),
("岡山製造事業體", "資材處", "原物料控制部", "作業員"),
("岡山製造事業體", "廠務與環安衛管理處", "", "處長"),
("岡山製造事業體", "廠務與環安衛管理處", "", "工程師"),
("岡山製造事業體", "廠務與環安衛管理處", "廠務部", "經副理"),
("岡山製造事業體", "廠務與環安衛管理處", "廠務部", "課長"),
("岡山製造事業體", "廠務與環安衛管理處", "廠務部", "工程師"),
("岡山製造事業體", "廠務與環安衛管理處", "廠務部", "專員"),
("產品事業體", "產品事業體", "廠務部", "處長"),
("產品事業體", "先進產品事業處", "", "處長"),
("產品事業體", "先進產品事業處", "產品管理部(APD)", "經副理"),
("產品事業體", "先進產品事業處", "產品管理部(APD)", "工程師"),
("產品事業體", "成熟產品事業處", "", "處長"),
("產品事業體", "成熟產品事業處", "產品管理部(MPD)", "經副理"),
("產品事業體", "成熟產品事業處", "產品管理部(MPD)", "專案經副理"),
("產品事業體", "成熟產品事業處", "產品管理部(MPD)", "工程師"),
("晶圓三廠", "晶圓三廠", "產品管理部(MPD)", "顧問"),
("晶圓三廠", "晶圓三廠", "產品管理部(MPD)", "專員"),
("晶圓三廠", "晶圓三廠", "品質部", "經副理"),
("晶圓三廠", "晶圓三廠", "品質部", "工程師"),
("晶圓三廠", "晶圓三廠", "品質部", "作業員"),
("晶圓三廠", "晶圓三廠", "製造部", "經副理"),
("晶圓三廠", "晶圓三廠", "製造部", "課長"),
("晶圓三廠", "晶圓三廠", "製造部", "班長"),
("晶圓三廠", "晶圓三廠", "製造部", "副班長"),
("晶圓三廠", "晶圓三廠", "製造部", "作業員"),
("晶圓三廠", "晶圓三廠", "廠務部(Fab3)", "經副理"),
("晶圓三廠", "晶圓三廠", "廠務部(Fab3)", "工程師"),
("晶圓三廠", "製程工程處", "工程一部", "經副理"),
("晶圓三廠", "製程工程處", "工程一部", "工程師"),
("晶圓三廠", "製程工程處", "工程二部", "經副理"),
("晶圓三廠", "製程工程處", "工程二部", "工程師"),
("晶圓三廠", "製程工程處", "工程三部", "經副理"),
("晶圓三廠", "製程工程處", "工程三部", "工程師"),
("晶圓三廠", "製程工程處", "製程整合部(Fab3)", "經副理"),
("晶圓三廠", "製程工程處", "製程整合部(Fab3)", "工程師"),
("集團人資行政事業體", "集團人資行政事業體", "製程整合部(Fab3)", "人資長"),
("集團人資行政事業體", "集團人資行政事業體", "行政總務管理部", "經副理"),
("集團人資行政事業體", "集團人資行政事業體", "行政總務管理部", "專員"),
("集團人資行政事業體", "集團人資行政事業體", "行政總務管理部", "助理"),
("集團人資行政事業體", "集團人資行政事業體", "招募任用部", "經副理"),
("集團人資行政事業體", "集團人資行政事業體", "招募任用部", "專員"),
("集團人資行政事業體", "集團人資行政事業體", "訓練發展部", "經副理"),
("集團人資行政事業體", "集團人資行政事業體", "訓練發展部", "專員"),
("集團人資行政事業體", "集團人資行政事業體", "薪酬管理部", "經副理"),
("集團人資行政事業體", "集團人資行政事業體", "薪酬管理部", "專員"),
("集團財務事業體", "集團財務事業體", "薪酬管理部", "財務長"),
("集團財務事業體", "岡山強茂財務處", "", "處長"),
("集團財務事業體", "岡山強茂財務處", "岡山強茂財務部", "經副理"),
("集團財務事業體", "岡山強茂財務處", "岡山強茂財務部", "課長"),
("集團財務事業體", "岡山強茂財務處", "岡山強茂財務部", "專員"),
("集團財務事業體", "集團財務事業體", "岡山強茂財務部", "專案副理"),
("集團會計事業體", "集團會計事業體", "岡山強茂財務部", "會計長"),
("集團會計事業體", "岡山會計處", "", "處長"),
("集團會計事業體", "岡山會計處", "會計部", "經副理"),
("集團會計事業體", "岡山會計處", "會計部", "課長"),
("集團會計事業體", "岡山會計處", "會計部", "專員"),
("集團會計事業體", "岡山會計處", "管理會計部", "經副理"),
("集團會計事業體", "岡山會計處", "管理會計部", "課長"),
("集團會計事業體", "岡山會計處", "管理會計部", "專員"),
("集團會計事業體", "集團會計處", "", "處長"),
("集團會計事業體", "集團會計處", "集團合併報表部", "經副理"),
("集團會計事業體", "集團會計處", "集團合併報表部", "專員"),
("集團資訊事業體", "集團資訊事業體", "集團合併報表部", "資訊長"),
("集團資訊事業體", "資安行動小組", "集團合併報表部", "課長"),
("集團資訊事業體", "資訊一處", "應用系統部", "經副理"),
("集團資訊事業體", "資訊一處", "應用系統部", "工程師"),
("集團資訊事業體", "資訊一處", "電腦整合製造部", "經副理"),
("集團資訊事業體", "資訊一處", "電腦整合製造部", "工程師"),
("集團資訊事業體", "資訊一處", "系統網路服務部", "經副理"),
("集團資訊事業體", "資訊一處", "系統網路服務部", "工程師"),
("集團資訊事業體", "資訊二處", "", "處長"),
("新創事業體", "新創事業體", "", "處長"),
("新創事業體", "新創事業體", "資源管理部", "經副理"),
("新創事業體", "新創事業體", "資源管理部", "專員"),
("新創事業體", "中低壓產品研發處", "", "經副理"),
("新創事業體", "研發中心", "", "工程師"),
("新創事業體", "高壓產品研發處", "", "經副理"),
("新創事業體", "研發中心", "", "工程師"),
("稽核室", "稽核室", "", "主任"),
("稽核室", "稽核室", "", "專員"),
("總經理室", "總經理室", "", "總裁"),
("總經理室", "總經理室", "", "總經理"),
("總經理室", "ESG專案辦公室", "", "經副理"),
("總經理室", "ESG專案辦公室", "", "課長"),
("總經理室", "ESG專案辦公室", "", "專員/工程師"),
("總經理室", "專案管理室", "", "副總經理"),
("總經理室", "專案管理室", "", "經副理"),
("總經理室", "專案管理室", "", "專員/工程師"),
("總品質事業體", "總品質事業體", "", "處長"),
("總品質事業體", "總品質事業體", "客戶品質管理部", "經副理"),
("總品質事業體", "總品質事業體", "客戶品質管理部", "課長"),
("總品質事業體", "總品質事業體", "客戶品質管理部", "工程師"),
("總品質事業體", "總品質事業體", "客戶品質管理部", "專員"),
("總品質事業體", "總品質事業體", "產品品質管理部", "經副理"),
("總品質事業體", "總品質事業體", "產品品質管理部", "課長"),
("總品質事業體", "總品質事業體", "產品品質管理部", "工程師"),
("總品質事業體", "總品質事業體", "產品品質管理部", "班長"),
("總品質事業體", "總品質事業體", "產品品質管理部", "作業員"),
("總品質事業體", "總品質事業體", "品質系統及客戶工程整合部", "經副理"),
("總品質事業體", "總品質事業體", "品質系統及客戶工程整合部", "課長"),
("總品質事業體", "總品質事業體", "品質系統及客戶工程整合部", "工程師"),
("總品質事業體", "總品質事業體", "封測外包品質管理部", "經副理"),
("總品質事業體", "總品質事業體", "封測外包品質管理部", "課長"),
("總品質事業體", "總品質事業體", "封測外包品質管理部", "工程師"),
("總品質事業體", "總品質事業體", "品質保證部", "經副理"),
("總品質事業體", "總品質事業體", "品質保證部", "課長"),
("總品質事業體", "總品質事業體", "品質保證部", "工程師"),
("總品質事業體", "總品質事業體", "品質保證部", "班長"),
("總品質事業體", "總品質事業體", "品質保證部", "副班長"),
("總品質事業體", "總品質事業體", "品質保證部", "作業員"),
("營業事業體", "營業事業體", "品質保證部", "副總經理"),
("營業事業體", "營業事業體", "品質保證部", "副總經理助理"),
("營業事業體", "商業開發暨市場應用處", "", "處長"),
("營業事業體", "商業開發暨市場應用處", "", "經理"),
("營業事業體", "商業開發暨市場應用處", "", "工程師"),
("營業事業體", "海外銷售事業處", "", "處長"),
("營業事業體", "海外銷售事業處", "日本區暨代工業務部", "經副理"),
("營業事業體", "海外銷售事業處", "日本區暨代工業務部", "課長"),
("營業事業體", "海外銷售事業處", "日本區暨代工業務部", "專員"),
("營業事業體", "海外銷售事業處", "日本區暨代工業務部", "助理"),
("營業事業體", "海外銷售事業處", "歐亞區業務部", "經副理"),
("營業事業體", "海外銷售事業處", "歐亞區業務部", "課長"),
("營業事業體", "海外銷售事業處", "歐亞區業務部", "專員"),
("營業事業體", "海外銷售事業處", "歐亞區業務部", "助理"),
("營業事業體", "海外銷售事業處", "韓國區業務部-韓國區", "經副理"),
("營業事業體", "海外銷售事業處", "韓國區業務部-韓國區", "課長"),
("營業事業體", "海外銷售事業處", "韓國區業務部-韓國區", "專員"),
("營業事業體", "海外銷售事業處", "韓國區業務部-韓國區", "助理"),
("營業事業體", "海外銷售事業處", "韓國區業務部-韓國區", "專案經理"),
("營業事業體", "海外銷售事業處", "美洲區業務部", "經副理"),
("營業事業體", "海外銷售事業處", "美洲區業務部", "課長"),
("營業事業體", "海外銷售事業處", "美洲區業務部", "專員"),
("營業事業體", "海外銷售事業處", "美洲區業務部", "助理"),
("營業事業體", "全球技術服務處", "", "處長"),
("營業事業體", "全球技術服務處", "", "工程師"),
("營業事業體", "全球技術服務處", "", "助理"),
("營業事業體", "全球技術服務處", "應用工程部(GTS)", "經副理"),
("營業事業體", "全球技術服務處", "應用工程部(GTS)", "專案經副理"),
("營業事業體", "全球技術服務處", "應用工程部(GTS)", "技術經副理"),
("營業事業體", "全球技術服務處", "應用工程部(GTS)", "工程師"),
("營業事業體", "全球技術服務處", "系統工程部", "經副理"),
("營業事業體", "全球技術服務處", "系統工程部", "工程師"),
("營業事業體", "全球技術服務處", "特性測試部", "經副理"),
("營業事業體", "全球技術服務處", "特性測試部", "課長"),
("營業事業體", "全球技術服務處", "特性測試部", "工程師"),
("營業事業體", "全球行銷暨業務支援處", "", "副總經理"),
("營業事業體", "全球行銷暨業務支援處", "業務生管部", "經副理"),
("營業事業體", "全球行銷暨業務支援處", "業務生管部", "課長"),
("營業事業體", "全球行銷暨業務支援處", "業務生管部", "專員"),
("營業事業體", "全球行銷暨業務支援處", "市場行銷企劃部", "處長"),
("營業事業體", "全球行銷暨業務支援處", "市場行銷企劃部", "經理"),
("營業事業體", "全球行銷暨業務支援處", "市場行銷企劃部", "專員"),
("營業事業體", "全球行銷暨業務支援處", "MOSFET晶圓採購部", "經副理"),
("營業事業體", "全球行銷暨業務支援處", "MOSFET晶圓採購部", "課長"),
("營業事業體", "全球行銷暨業務支援處", "MOSFET晶圓採購部", "專員"),
("營業事業體", "大中華區銷售事業處", "", "處長"),
("營業事業體", "大中華區銷售事業處", "台灣區業務部", "專員"),
("營業事業體", "大中華區銷售事業處", "台灣區業務部", "助理"),
("營業事業體", "大中華區銷售事業處", "業務一部", "處長/資深經理"),
("營業事業體", "大中華區銷售事業處", "業務一部", "經副理"),
("營業事業體", "大中華區銷售事業處", "業務一部", "專員"),
("營業事業體", "大中華區銷售事業處", "業務一部", "助理"),
("營業事業體", "大中華區銷售事業處", "業務二部", "處長/資深經理"),
("營業事業體", "大中華區銷售事業處", "業務二部", "經副理"),
("營業事業體", "大中華區銷售事業處", "業務二部", "專員"),
("營業事業體", "大中華區銷售事業處", "業務二部", "助理"),
]
# 建立結構化資料
HR_position_business_units = {} # 事業體
HR_position_divisions = {} # 處級單位
HR_position_departments = {} # 部級單位
HR_position_organization_positions = [] # 完整組織崗位關聯
# 關聯映射
businessToDivision = {} # 事業體 -> 處級單位列表
divisionToDepartment = {} # 處級單位 -> 部級單位列表
departmentToPosition = {} # 部級單位 -> 崗位列表
business_id = 1
division_id = 1
department_id = 1
position_id = 1
for business, division, department, position in raw_data:
# 處理事業體
if business and business not in HR_position_business_units:
HR_position_business_units[business] = {
'id': business_id,
'code': f'BU{str(business_id).zfill(3)}',
'name': business
}
business_id += 1
businessToDivision[business] = []
# 處理處級單位
if division and division not in HR_position_divisions:
HR_position_divisions[division] = {
'id': division_id,
'code': f'DIV{str(division_id).zfill(3)}',
'name': division,
'business': business
}
division_id += 1
divisionToDepartment[division] = []
# 建立事業體到處級的關聯
if business and division and division not in businessToDivision.get(business, []):
if business not in businessToDivision:
businessToDivision[business] = []
businessToDivision[business].append(division)
# 處理部級單位(可能為空)
dept_key = department if department else f"(直屬){division}"
if dept_key not in HR_position_departments:
HR_position_departments[dept_key] = {
'id': department_id,
'code': f'DEPT{str(department_id).zfill(3)}',
'name': department if department else '(直屬)',
'division': division
}
department_id += 1
departmentToPosition[dept_key] = []
# 建立處級到部級的關聯
if division and dept_key not in divisionToDepartment.get(division, []):
if division not in divisionToDepartment:
divisionToDepartment[division] = []
divisionToDepartment[division].append(dept_key)
# 處理崗位(去重)
if position and position not in departmentToPosition.get(dept_key, []):
departmentToPosition[dept_key].append(position)
# 建立完整組織崗位關聯
HR_position_organization_positions.append({
'id': position_id,
'business': business,
'division': division,
'department': department if department else '(直屬)',
'position': position
})
position_id += 1
return {
'HR_position_business_units': HR_position_business_units,
'HR_position_divisions': HR_position_divisions,
'HR_position_departments': HR_position_departments,
'HR_position_organization_positions': HR_position_organization_positions,
'businessToDivision': businessToDivision,
'divisionToDepartment': divisionToDepartment,
'departmentToPosition': departmentToPosition
}
if __name__ == '__main__':
# 測試
data = import_to_memory()
print(f"事業體數量: {len(data['HR_position_business_units'])}")
print(f"處級單位數量: {len(data['HR_position_divisions'])}")
print(f"部級單位數量: {len(data['HR_position_departments'])}")
print(f"組織崗位關聯數量: {len(data['HR_position_organization_positions'])}")
print()
print("事業體列表:")
for name, info in data['HR_position_business_units'].items():
print(f" - {info['id']}: {name}")

3528
index.html

File diff suppressed because it is too large Load Diff

406
js/admin.js Normal file
View File

@@ -0,0 +1,406 @@
/**
* 管理功能模組
* 處理使用者管理、系統設定和統計功能
*/
import { CSVUtils } from './csv.js';
const API_BASE_URL = '/api';
// ==================== 使用者管理 ====================
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;
export 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';
}
export 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';
}
export function closeUserModal() {
document.getElementById('userModal').style.display = 'none';
editingUserId = null;
}
export 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) {
if (typeof showToast === 'function') showToast('請填寫所有必填欄位');
return;
}
if (editingUserId) {
const index = usersData.findIndex(u => u.employeeId === editingUserId);
if (index > -1) {
usersData[index] = { ...usersData[index], name, email, role };
if (typeof showToast === 'function') showToast('使用者已更新');
}
} else {
if (usersData.some(u => u.employeeId === employeeId)) {
if (typeof showToast === 'function') showToast('工號已存在');
return;
}
usersData.push({
employeeId,
name,
email,
role,
createdAt: new Date().toISOString().split('T')[0]
});
if (typeof showToast === 'function') showToast('使用者已新增');
}
closeUserModal();
renderUserList();
}
export function deleteUser(employeeId) {
if (confirm('確定要刪除此使用者嗎?')) {
usersData = usersData.filter(u => u.employeeId !== employeeId);
renderUserList();
if (typeof showToast === 'function') showToast('使用者已刪除');
}
}
export function renderUserList() {
const tbody = document.getElementById('userListBody');
if (!tbody) return;
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;">${sanitizeHTML(user.employeeId)}</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">${sanitizeHTML(user.name)}</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">${sanitizeHTML(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;">${sanitizeHTML(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('');
}
export function exportUsersCSV() {
const headers = ['employeeId', 'name', 'email', 'role', 'createdAt'];
CSVUtils.exportToCSV(usersData, 'users.csv', headers);
if (typeof showToast === 'function') showToast('使用者清單已匯出!');
}
// ==================== 用戶信息與登出功能 ====================
export function loadUserInfo() {
const currentUser = localStorage.getItem('currentUser');
if (currentUser) {
try {
const user = JSON.parse(currentUser);
const userNameEl = document.getElementById('userName');
if (userNameEl) {
userNameEl.textContent = user.name || user.username;
}
const userRoleEl = document.getElementById('userRole');
if (userRoleEl) {
let roleText = '';
switch(user.role) {
case 'user':
roleText = '一般使用者 ★☆☆';
break;
case 'admin':
roleText = '管理者 ★★☆';
break;
case 'superadmin':
roleText = '最高管理者 ★★★';
break;
default:
roleText = '一般使用者';
}
userRoleEl.textContent = roleText;
}
const userAvatarEl = document.getElementById('userAvatar');
if (userAvatarEl) {
const avatarText = (user.name || user.username || 'U').charAt(0).toUpperCase();
userAvatarEl.textContent = avatarText;
}
console.log('用戶信息已載入:', user.name, user.role);
} catch (error) {
console.error('載入用戶信息失敗:', error);
}
} else {
console.warn('未找到用戶信息,重定向到登入頁面');
window.location.href = 'login.html';
}
}
export function logout() {
if (confirm('確定要登出系統嗎?')) {
localStorage.removeItem('currentUser');
if (typeof showToast === 'function') showToast('已成功登出系統');
setTimeout(() => {
window.location.href = 'login.html';
}, 1000);
}
}
// ==================== Ollama 模型設定 ====================
export function saveOllamaModel() {
const saveBtn = document.getElementById('saveModelBtn');
if (saveBtn) {
saveBtn.style.display = 'inline-flex';
}
hideAllMessages();
}
export function saveOllamaModelWithConfirmation() {
const reasonerRadio = document.getElementById('model-reasoner');
const chatRadio = document.getElementById('model-chat');
let selectedModel = '';
if (reasonerRadio && reasonerRadio.checked) {
selectedModel = 'deepseek-reasoner';
} else if (chatRadio && chatRadio.checked) {
selectedModel = 'deepseek-chat';
}
if (selectedModel) {
localStorage.setItem('ollamaModel', selectedModel);
const saveBtn = document.getElementById('saveModelBtn');
if (saveBtn) {
saveBtn.style.display = 'none';
}
hideAllMessages();
const successDiv = document.getElementById('modelSaveSuccess');
if (successDiv) {
successDiv.style.display = 'block';
setTimeout(() => {
successDiv.style.display = 'none';
}, 3000);
}
console.log('Ollama 模型已設定為:', selectedModel);
}
}
export function loadOllamaModel() {
const savedModel = localStorage.getItem('ollamaModel') || 'deepseek-reasoner';
const reasonerRadio = document.getElementById('model-reasoner');
const chatRadio = document.getElementById('model-chat');
const gptossRadio = document.getElementById('model-gptoss');
if (savedModel === 'deepseek-reasoner' && reasonerRadio) {
reasonerRadio.checked = true;
} else if (savedModel === 'deepseek-chat' && chatRadio) {
chatRadio.checked = true;
} else if (savedModel === 'gpt-oss:120b' && gptossRadio) {
gptossRadio.checked = true;
}
console.log('已載入 Ollama 模型設定:', savedModel);
}
export function getOllamaModel() {
return localStorage.getItem('ollamaModel') || 'deepseek-reasoner';
}
export function hideAllMessages() {
const messages = ['connectionSuccess', 'connectionError', 'modelSaveSuccess'];
messages.forEach(id => {
const elem = document.getElementById(id);
if (elem) elem.style.display = 'none';
});
}
export async function testOllamaConnection() {
const testBtn = document.getElementById('testConnectionBtn');
const reasonerRadio = document.getElementById('model-reasoner');
const chatRadio = document.getElementById('model-chat');
let selectedModel = '';
if (reasonerRadio && reasonerRadio.checked) {
selectedModel = 'deepseek-reasoner';
} else if (chatRadio && chatRadio.checked) {
selectedModel = 'deepseek-chat';
}
if (!selectedModel) {
if (typeof showToast === 'function') showToast('請先選擇一個模型');
return;
}
if (testBtn) {
testBtn.disabled = true;
testBtn.textContent = '測試中...';
}
hideAllMessages();
try {
const response = await fetch('/api/llm/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api: 'ollama',
model: selectedModel,
prompt: '請用一句話說明你是誰',
max_tokens: 100
}),
timeout: 30000
});
const data = await response.json();
if (data.success) {
const successDiv = document.getElementById('connectionSuccess');
if (successDiv) {
successDiv.style.display = 'block';
setTimeout(() => {
successDiv.style.display = 'none';
}, 5000);
}
console.log('測試回應:', data.text);
} else {
throw new Error(data.error || '未知錯誤');
}
} catch (error) {
const errorDiv = document.getElementById('connectionError');
const errorMsg = document.getElementById('connectionErrorMessage');
if (errorDiv && errorMsg) {
errorMsg.textContent = error.message;
errorDiv.style.display = 'block';
setTimeout(() => {
errorDiv.style.display = 'none';
}, 10000);
}
console.error('連線測試失敗:', error);
} finally {
if (testBtn) {
testBtn.disabled = false;
testBtn.innerHTML = `
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;">
<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>
測試連線
`;
}
}
}
// ==================== 崗位資料管理功能 ====================
export async function exportCompletePositionData() {
try {
if (typeof showToast === 'function') showToast('正在準備匯出資料...');
window.location.href = API_BASE_URL + '/position-list/export';
setTimeout(() => {
if (typeof showToast === 'function') showToast('崗位資料匯出成功!');
}, 1000);
} catch (error) {
console.error('匯出錯誤:', error);
alert('匯出失敗: ' + error.message);
}
}
export async function refreshPositionStats() {
try {
if (typeof showToast === 'function') showToast('正在更新統計資料...');
const response = await fetch(API_BASE_URL + '/position-list');
const result = await response.json();
if (result.success) {
const positions = result.data;
const total = positions.length;
const described = positions.filter(p => p.hasDescription).length;
const undescribed = total - described;
const totalEl = document.getElementById('totalPositionsCount');
const describedEl = document.getElementById('describedPositionsCount');
const undescribedEl = document.getElementById('undescribedPositionsCount');
if (totalEl) totalEl.textContent = total;
if (describedEl) describedEl.textContent = described;
if (undescribedEl) undescribedEl.textContent = undescribed;
if (typeof showToast === 'function') showToast('統計資料已更新');
} else {
alert('更新統計失敗: ' + (result.error || '未知錯誤'));
}
} catch (error) {
console.error('更新統計錯誤:', error);
alert('更新統計失敗: ' + error.message);
}
}
// ==================== 工具函數 ====================
function sanitizeHTML(str) {
if (str === null || str === undefined) return '';
const temp = document.createElement('div');
temp.textContent = str;
return temp.innerHTML;
}
// 暴露到全域
if (typeof window !== 'undefined') {
window.showAddUserModal = showAddUserModal;
window.editUser = editUser;
window.closeUserModal = closeUserModal;
window.saveUser = saveUser;
window.deleteUser = deleteUser;
window.renderUserList = renderUserList;
window.exportUsersCSV = exportUsersCSV;
window.logout = logout;
window.testOllamaConnection = testOllamaConnection;
window.saveOllamaModelWithConfirmation = saveOllamaModelWithConfirmation;
window.exportCompletePositionData = exportCompletePositionData;
window.refreshPositionStats = refreshPositionStats;
}

628
js/ai-bags.js Normal file
View File

@@ -0,0 +1,628 @@
/**
* AI Bags - 三個錦囊功能
* 提供可自定義 prompt 的 AI 生成按鈕
*/
import { callClaudeAPI } from './api.js';
import { showToast, fillIfEmpty } from './utils.js';
import { getPositionFormData, getJobFormData, getJobDescFormData, getDeptFunctionFormData } from './ui.js';
// ==================== 預設 Prompt 模板 ====================
const DEFAULT_PROMPTS = {
positionBasic: {
bag1: {
title: '簡化版',
subtitle: '僅必填欄位',
prompt: `你是專業人資顧問,熟悉半導體製造業。請生成崗位基礎資料(僅必填欄位)。
已填寫的資料:{existingData}
需要生成的欄位positionCode, positionName
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag2: {
title: '標準版',
subtitle: '常用欄位',
prompt: `你是專業人資顧問,熟悉半導體製造業。請生成崗位基礎資料(標準版)。
已填寫的資料:{existingData}
需要生成的欄位positionCode, positionName, positionCategory, positionLevel, headcount
欄位說明:
- positionCode: 崗位編號(格式如 ENG-001
- positionName: 崗位名稱
- positionCategory: 崗位類別代碼01=技術職, 02=管理職, 03=業務職, 04=行政職)
- positionLevel: 崗位級別L1-L7
- headcount: 編制人數1-10
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag3: {
title: '詳細版',
subtitle: '所有欄位',
prompt: `你是專業人資顧問,熟悉半導體製造業的人資所有流程。請生成完整的崗位基礎資料。
已填寫的資料:{existingData}
需要生成的欄位positionCode, positionName, positionCategory, positionNature, headcount, positionLevel, positionDesc, positionRemark
欄位說明:
- positionCode: 崗位編號(格式如 ENG-001, MGR-002
- positionName: 崗位名稱
- positionCategory: 崗位類別代碼01=技術職, 02=管理職, 03=業務職, 04=行政職)
- positionNature: 崗位性質代碼FT=全職, PT=兼職, CT=約聘, IN=實習)
- headcount: 編制人數1-10之間的數字字串
- positionLevel: 崗位級別L1到L7
- positionDesc: 崗位描述(條列式,用換行分隔)
- positionRemark: 崗位備注(條列式,用換行分隔)
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
}
},
positionRecruit: {
bag1: {
title: '基本需求',
subtitle: '核心要求',
prompt: `請生成「{positionName}」的基本招聘要求。
已填寫的資料:{existingData}
需要生成的欄位minEducation, workExperience, jobType, jobTitle
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag2: {
title: '標準需求',
subtitle: '完整資訊',
prompt: `請生成「{positionName}」的標準招聘要求。
已填寫的資料:{existingData}
需要生成的欄位minEducation, salaryRange, workExperience, jobType, recruitPosition, jobTitle, positionReq, skillReq
欄位說明:
- minEducation: 最低學歷代碼HS=高中職, JC=專科, BA=大學, MA=碩士, PHD=博士)
- salaryRange: 薪酬范圍代碼A=30000以下, B=30000-50000, C=50000-80000, D=80000-120000, E=120000以上, N=面議)
- workExperience: 工作經驗年數0=不限, 1, 3, 5, 10
- jobType: 工作性質代碼FT=全職, PT=兼職, CT=約聘, DP=派遣)
- recruitPosition: 招聘職位代碼ENG=工程師, MGR=經理, AST=助理, OP=作業員, SAL=業務)
- positionReq: 崗位要求(條列式,用換行分隔)
- skillReq: 技能要求(條列式,用換行分隔)
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag3: {
title: '完整需求',
subtitle: '18個欄位',
prompt: `請生成「{positionName}」的完整招聘要求資料。
已填寫的資料:{existingData}
需要生成所有空白欄位。
欄位說明:
- minEducation: 最低學歷代碼HS=高中職, JC=專科, BA=大學, MA=碩士, PHD=博士)
- requiredGender: 性別要求代碼M=限男性, F=限女性, N=不限)
- salaryRange: 薪酬范圍代碼A=30000以下, B=30000-50000, C=50000-80000, D=80000-120000, E=120000以上, N=面議)
- workExperience: 工作經驗年數0=不限, 1, 3, 5, 10
- minAge: 最低年齡18-65
- maxAge: 最高年齡18-65
- jobType: 工作性質代碼FT=全職, PT=兼職, CT=約聘, DP=派遣)
- recruitPosition: 招聘職位代碼ENG=工程師, MGR=經理, AST=助理, OP=作業員, SAL=業務)
- jobTitle: 職位名稱
- jobDesc: 工作內容(條列式,用換行分隔)
- positionReq: 崗位要求(條列式,用換行分隔)
- titleReq: 職稱要求(條列式,用換行分隔)
- majorReq: 科系要求(多個科系用逗號分隔)
- skillReq: 技能要求(條列式,用換行分隔)
- langReq: 語言要求(條列式,用換行分隔)
- otherReq: 其他要求(條列式,用換行分隔)
- superiorPosition: 直屬主管職位
- recruitRemark: 招聘備注
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
}
},
jobBasic: {
bag1: {
title: '簡化版',
subtitle: '核心欄位',
prompt: `請生成職務基礎資料(簡化版)。
已填寫的資料:{existingData}
需要生成的欄位jobCode, jobName, jobCategoryCode
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag2: {
title: '標準版',
subtitle: '常用欄位',
prompt: `請生成職務基礎資料(標準版)。
已填寫的資料:{existingData}
需要生成的欄位jobCode, jobName, jobNameEn, jobCategoryCode, jobLevel, jobHeadcount
欄位說明:
- jobCode: 職務編號(格式如 J001
- jobName: 職務名稱(繁體中文)
- jobNameEn: 職務名稱英文
- jobCategoryCode: 職務類別代碼01=技術類, 02=管理類, 03=業務類, 04=行政類)
- jobLevel: 職務級別J1-J7
- jobHeadcount: 職務人數1-100
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag3: {
title: '完整版',
subtitle: '所有欄位',
prompt: `請生成完整的職務基礎資料。
已填寫的資料:{existingData}
需要生成所有空白欄位。
欄位說明:
- jobCode: 職務編號(格式如 J001
- jobName: 職務名稱(繁體中文)
- jobNameEn: 職務名稱英文
- jobCategoryCode: 職務類別代碼01=技術類, 02=管理類, 03=業務類, 04=行政類)
- jobLevel: 職務級別J1-J7
- jobHeadcount: 職務人數1-100
- jobEffectiveDate: 生效日期YYYY-MM-DD
- jobSortOrder: 排序順序1-999
- hasAttendanceBonus: 是否有全勤獎金true/false
- hasHousingAllowance: 是否有住宿津貼true/false
- jobRemark: 職務備註
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
}
},
deptFunction: {
bag1: {
title: '基本版',
subtitle: '核心資訊',
prompt: `請生成部門職責資料(基本版)。
已填寫的資料:{existingData}
需要生成的欄位deptFunctionCode, deptFunctionName, deptFunctionBU, deptFunctionDept
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag2: {
title: '標準版',
subtitle: '含職責描述',
prompt: `請生成部門職責資料(標準版)。
已填寫的資料:{existingData}
需要生成的欄位deptFunctionCode, deptFunctionName, deptFunctionBU, deptFunctionDept, deptManager, deptMission, deptCoreFunctions
欄位說明:
- deptFunctionCode: 部門職責編號(格式如 DF001
- deptFunctionName: 部門職責名稱
- deptFunctionBU: 事業單位代碼BU1-BU5
- deptFunctionDept: 部門代碼DEPT1-DEPT20
- deptManager: 部門主管
- deptMission: 部門使命(簡短描述)
- deptCoreFunctions: 核心職能(條列式,用換行分隔)
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag3: {
title: '完整版',
subtitle: '含KPI指標',
prompt: `請生成完整的部門職責資料。
已填寫的資料:{existingData}
需要生成所有空白欄位。
欄位說明:
- deptFunctionCode: 部門職責編號(格式如 DF001
- deptFunctionName: 部門職責名稱
- deptFunctionBU: 事業單位代碼BU1-BU5
- deptFunctionDept: 部門代碼DEPT1-DEPT20
- deptManager: 部門主管
- deptMission: 部門使命(簡短描述)
- deptVision: 部門願景(條列式,用換行分隔)
- deptCoreFunctions: 核心職能(條列式,用換行分隔)
- deptKPIs: KPI 指標(條列式,用換行分隔)
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
}
},
jobDesc: {
bag1: {
title: '基本版',
subtitle: '核心資訊',
prompt: `請生成崗位描述資料(基本版)。
已填寫的資料:{existingData}
需要生成的欄位positionName, department, positionPurpose
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag2: {
title: '標準版',
subtitle: '含職責說明',
prompt: `請生成崗位描述資料(標準版)。
已填寫的資料:{existingData}
需要生成的欄位positionName, department, directSupervisor, positionPurpose, mainResponsibilities, education, basicSkills
欄位說明:
- positionName: 崗位名稱
- department: 所屬部門
- directSupervisor: 直屬主管
- positionPurpose: 崗位目的(簡短描述)
- mainResponsibilities: 主要職責(條列式,用換行分隔)
- education: 學歷要求
- basicSkills: 基本技能(條列式,用換行分隔)
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
},
bag3: {
title: '完整版',
subtitle: '33個欄位',
prompt: `請生成完整的崗位描述資料。
已填寫的資料:{existingData}
需要生成所有空白欄位。
包含以下區塊:
1. 基本資訊empNo, empName, positionCode, versionDate
2. 崗位資訊positionName, department, positionEffectiveDate, directSupervisor, positionGradeJob, reportTo, directReports, workLocation, empAttribute
3. 職責與目的positionPurpose, mainResponsibilities
4. 任職要求education, basicSkills, professionalKnowledge, workExperienceReq, otherRequirements
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
}
}
};
// ==================== LocalStorage 管理 ====================
/**
* 獲取模組的 prompts優先使用自定義否則使用預設
* @param {string} module - 模組名稱
* @returns {Object} - 包含三個 bag 的 prompt 物件
*/
function getModulePrompts(module) {
try {
const saved = localStorage.getItem('aiPrompts');
const prompts = saved ? JSON.parse(saved) : {};
// 如果沒有保存的 prompts使用預設值
if (!prompts[module]) {
return DEFAULT_PROMPTS[module] || {};
}
return prompts[module];
} catch (e) {
console.error('讀取 prompts 失敗:', e);
return DEFAULT_PROMPTS[module] || {};
}
}
/**
* 保存模組的 prompts
* @param {string} module - 模組名稱
* @param {number} bagNumber - 錦囊編號 (1-3)
* @param {Object} bagData - 包含 title, subtitle, prompt 的物件
*/
function saveModulePrompt(module, bagNumber, bagData) {
try {
const saved = localStorage.getItem('aiPrompts');
const prompts = saved ? JSON.parse(saved) : {};
if (!prompts[module]) {
prompts[module] = {};
}
prompts[module][`bag${bagNumber}`] = bagData;
localStorage.setItem('aiPrompts', JSON.stringify(prompts));
return true;
} catch (e) {
console.error('保存 prompt 失敗:', e);
return false;
}
}
/**
* 初始化所有模組的預設 prompts如果尚未設定
*/
function initializeDefaultPrompts() {
const saved = localStorage.getItem('aiPrompts');
if (!saved) {
localStorage.setItem('aiPrompts', JSON.stringify(DEFAULT_PROMPTS));
}
}
// ==================== 執行 AI 錦囊 ====================
/**
* 執行 AI 錦囊
* @param {string} module - 模組名稱
* @param {number} bagNumber - 錦囊編號 (1-3)
*/
export async function executeAIBag(module, bagNumber) {
const bagElement = document.querySelector(`.ai-bag[data-module="${module}"][data-bag="${bagNumber}"]`);
if (!bagElement) return;
// 防止重複點擊
if (bagElement.classList.contains('loading')) return;
try {
// 顯示載入狀態
bagElement.classList.add('loading');
const icon = bagElement.querySelector('.bag-icon');
const originalIcon = icon.textContent;
icon.innerHTML = '<div class="spinner"></div>';
// 獲取當前表單資料
let existingData = {};
if (module === 'positionBasic' || module === 'positionRecruit') {
const positionData = getPositionFormData();
existingData = module === 'positionBasic' ? positionData.basicInfo : positionData.recruitInfo;
} else if (module === 'jobBasic') {
existingData = getJobFormData();
} else if (module === 'jobDesc') {
existingData = getJobDescFormData();
} else if (module === 'deptFunction') {
existingData = getDeptFunctionFormData();
}
// 獲取 prompt 模板
const prompts = getModulePrompts(module);
const bagPrompt = prompts[`bag${bagNumber}`];
if (!bagPrompt || !bagPrompt.prompt) {
throw new Error('Prompt 模板不存在');
}
// 替換 prompt 中的變數
let finalPrompt = bagPrompt.prompt
.replace('{existingData}', JSON.stringify(existingData, null, 2))
.replace('{positionName}', existingData.positionName || '此崗位');
// 調用 AI API
const result = await callClaudeAPI(finalPrompt);
// 填充表單
fillFormWithAIResult(module, result);
showToast('✨ AI 生成成功!');
} catch (error) {
console.error('AI 錦囊執行失敗:', error);
showToast('❌ AI 生成失敗: ' + error.message);
} finally {
// 恢復正常狀態
bagElement.classList.remove('loading');
const icon = bagElement.querySelector('.bag-icon');
icon.textContent = '🎁';
}
}
/**
* 根據 AI 結果填充表單
* @param {string} module - 模組名稱
* @param {Object} result - AI 返回的 JSON 結果
*/
function fillFormWithAIResult(module, result) {
if (module === 'positionBasic') {
// 崗位基礎資料 - 基礎資料頁籤
Object.entries(result).forEach(([key, value]) => {
fillIfEmpty(key, value);
});
} else if (module === 'positionRecruit') {
// 崗位基礎資料 - 招聘要求頁籤
Object.entries(result).forEach(([key, value]) => {
fillIfEmpty(key, value);
});
} else if (module === 'jobBasic') {
// 職務基礎資料
Object.entries(result).forEach(([key, value]) => {
if (key === 'hasAttendanceBonus' || key === 'hasHousingAllowance') {
const checkbox = document.getElementById(key);
if (checkbox) checkbox.checked = value;
} else {
fillIfEmpty(key, value);
}
});
} else if (module === 'deptFunction') {
// 部門職責
Object.entries(result).forEach(([key, value]) => {
fillIfEmpty('df_' + key, value);
});
} else if (module === 'jobDesc') {
// 崗位描述
if (result.basicInfo) {
Object.entries(result.basicInfo).forEach(([key, value]) => {
fillIfEmpty('jd_' + key, value);
});
}
if (result.positionInfo) {
Object.entries(result.positionInfo).forEach(([key, value]) => {
fillIfEmpty('jd_' + key, value);
});
}
if (result.responsibilities) {
Object.entries(result.responsibilities).forEach(([key, value]) => {
fillIfEmpty('jd_' + key, value);
});
}
if (result.requirements) {
Object.entries(result.requirements).forEach(([key, value]) => {
fillIfEmpty('jd_' + key, value);
});
}
}
}
// ==================== 編輯 Prompt ====================
/**
* 編輯錦囊 Prompt
* @param {Event} event - 點擊事件
* @param {string} module - 模組名稱
* @param {number} bagNumber - 錦囊編號 (1-3)
*/
export function editBagPrompt(event, module, bagNumber) {
event.stopPropagation(); // 防止觸發父元素的 click 事件
const prompts = getModulePrompts(module);
const bagPrompt = prompts[`bag${bagNumber}`];
if (!bagPrompt) {
showToast('❌ Prompt 不存在');
return;
}
// 顯示編輯對話框
showPromptEditModal(module, bagNumber, bagPrompt);
}
/**
* 顯示 Prompt 編輯對話框
* @param {string} module - 模組名稱
* @param {number} bagNumber - 錦囊編號 (1-3)
* @param {Object} bagData - 包含 title, subtitle, prompt 的物件
*/
function showPromptEditModal(module, bagNumber, bagData) {
const modal = document.getElementById('promptEditModal');
if (!modal) {
console.error('找不到編輯對話框');
return;
}
// 填充對話框內容
document.getElementById('promptModalTitle').textContent = `編輯錦囊 ${bagNumber} - ${bagData.title}`;
document.getElementById('promptTitle').value = bagData.title || '';
document.getElementById('promptSubtitle').value = bagData.subtitle || '';
document.getElementById('promptContent').value = bagData.prompt || '';
// 保存當前編輯的模組和錦囊編號
modal.dataset.module = module;
modal.dataset.bagNumber = bagNumber;
// 顯示對話框
modal.classList.add('show');
}
/**
* 保存編輯的 Prompt
*/
export function savePromptEdit() {
const modal = document.getElementById('promptEditModal');
const module = modal.dataset.module;
const bagNumber = parseInt(modal.dataset.bagNumber);
const title = document.getElementById('promptTitle').value.trim();
const subtitle = document.getElementById('promptSubtitle').value.trim();
const prompt = document.getElementById('promptContent').value.trim();
if (!title || !prompt) {
showToast('⚠️ 標題和 Prompt 內容不能為空');
return;
}
// 保存到 LocalStorage
const success = saveModulePrompt(module, bagNumber, { title, subtitle, prompt });
if (success) {
// 更新頁面上的錦囊標題
const bagElement = document.querySelector(`.ai-bag[data-module="${module}"][data-bag="${bagNumber}"]`);
if (bagElement) {
const titleElement = bagElement.querySelector('.bag-title');
const subtitleElement = bagElement.querySelector('.bag-subtitle');
if (titleElement) titleElement.textContent = title;
if (subtitleElement) {
if (subtitle) {
if (!subtitleElement.classList) {
const newSubtitle = document.createElement('div');
newSubtitle.className = 'bag-subtitle';
newSubtitle.textContent = subtitle;
bagElement.querySelector('.bag-title').after(newSubtitle);
} else {
subtitleElement.textContent = subtitle;
}
}
}
}
closePromptEditModal();
showToast('✅ Prompt 已保存');
} else {
showToast('❌ 保存失敗');
}
}
/**
* 關閉編輯對話框
*/
export function closePromptEditModal() {
const modal = document.getElementById('promptEditModal');
if (modal) {
modal.classList.remove('show');
}
}
/**
* 重置為預設 Prompt
*/
export function resetToDefaultPrompt() {
const modal = document.getElementById('promptEditModal');
const module = modal.dataset.module;
const bagNumber = parseInt(modal.dataset.bagNumber);
const defaultPrompt = DEFAULT_PROMPTS[module]?.[`bag${bagNumber}`];
if (!defaultPrompt) {
showToast('❌ 找不到預設 Prompt');
return;
}
if (confirm('確定要重置為預設 Prompt 嗎?')) {
document.getElementById('promptTitle').value = defaultPrompt.title || '';
document.getElementById('promptSubtitle').value = defaultPrompt.subtitle || '';
document.getElementById('promptContent').value = defaultPrompt.prompt || '';
showToast('✅ 已重置為預設值');
}
}
// ==================== 初始化 ====================
/**
* 初始化錦囊標題
*/
export function initializeBagTitles() {
document.querySelectorAll('.ai-bag').forEach(bag => {
const module = bag.dataset.module;
const bagNumber = parseInt(bag.dataset.bag);
const prompts = getModulePrompts(module);
const bagPrompt = prompts[`bag${bagNumber}`];
if (bagPrompt) {
const titleElement = bag.querySelector('.bag-title');
const subtitleElement = bag.querySelector('.bag-subtitle');
if (titleElement) titleElement.textContent = bagPrompt.title || `錦囊${bagNumber}`;
if (subtitleElement && bagPrompt.subtitle) {
subtitleElement.textContent = bagPrompt.subtitle;
}
}
});
}
// 頁面載入時初始化
document.addEventListener('DOMContentLoaded', () => {
initializeDefaultPrompts();
initializeBagTitles();
});
// ==================== 掛載到 window ====================
if (typeof window !== 'undefined') {
window.executeAIBag = executeAIBag;
window.editBagPrompt = editBagPrompt;
window.savePromptEdit = savePromptEdit;
window.closePromptEditModal = closePromptEditModal;
window.resetToDefaultPrompt = resetToDefaultPrompt;
}

773
js/ai.js Normal file
View File

@@ -0,0 +1,773 @@
/**
* AI 生成功能模組
* 處理 LLM API 調用和表單自動填充
*/
import { DEFAULT_AI_PROMPTS } from './prompts.js';
// ==================== 工具函數 ====================
/**
* 消毒 HTML 字串,防止 XSS 攻擊
*/
export function sanitizeHTML(str) {
if (str === null || str === undefined) return '';
const temp = document.createElement('div');
temp.textContent = str;
return temp.innerHTML;
}
/**
* 安全設定元素文字內容
*/
export function safeSetText(element, text) {
if (element) {
element.textContent = text;
}
}
/**
* 只在欄位為空時填入值
*/
export function fillIfEmpty(elementId, value) {
const el = document.getElementById(elementId);
if (el && !el.value.trim() && value) {
el.value = value;
return true;
}
return false;
}
/**
* 獲取欄位當前值
*/
export function getFieldValue(elementId) {
const el = document.getElementById(elementId);
return el ? el.value.trim() : '';
}
/**
* 獲取空白欄位列表
*/
export function getEmptyFields(fieldIds) {
return fieldIds.filter(id => !getFieldValue(id));
}
/**
* 獲取 Ollama 模型選項
*/
export function getOllamaModel() {
const select = document.getElementById('ollamaModel');
return select ? select.value : 'llama3.2';
}
// ==================== AI API 調用 ====================
/**
* 調用 LLM API
*/
export async function callClaudeAPI(prompt, api = 'ollama') {
try {
const requestData = {
api: api,
prompt: prompt,
max_tokens: 2000
};
if (api === 'ollama') {
requestData.model = getOllamaModel();
}
const response = await fetch("/api/llm/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestData)
});
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;
// 移除 markdown 代碼塊標記
responseText = responseText.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
// 嘗試提取 JSON 物件
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const allMatches = responseText.match(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g);
if (allMatches && allMatches.length > 0) {
for (let i = allMatches.length - 1; i >= 0; i--) {
try {
const parsed = JSON.parse(allMatches[i]);
if (Object.keys(parsed).length > 0) {
return parsed;
}
} catch (e) {
continue;
}
}
}
try {
return JSON.parse(jsonMatch[0]);
} catch (e) {
// 繼續嘗試其他方法
}
}
return JSON.parse(responseText);
} catch (error) {
console.error("Error calling LLM API:", error);
let errorDetails = error.message;
try {
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 app.py)',
'已在 .env 文件中配置有效的 LLM API Key',
'網路連線正常',
'確認 Prompt 要求返回純 JSON 格式',
'嘗試使用不同的 LLM API (切換到其他模型)',
'檢查 LLM 模型是否支援繁體中文'
]
});
throw error;
}
}
// ==================== 按鈕狀態控制 ====================
export function setButtonLoading(btn, loading) {
if (loading) {
btn.disabled = true;
btn.innerHTML = '<div class="spinner"></div><span>AI 生成中...</span>';
} else {
btn.disabled = false;
btn.innerHTML = '<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>';
}
}
// ==================== AI 幫我想功能 ====================
/**
* 初始化所有 prompt 編輯器
*/
export function initializePromptEditors() {
const modules = Object.keys(DEFAULT_AI_PROMPTS);
modules.forEach(module => {
const textarea = document.getElementById(`prompt_${module}`);
if (textarea) {
const savedPrompt = localStorage.getItem(`aiPrompt_${module}`);
textarea.value = savedPrompt || DEFAULT_AI_PROMPTS[module];
textarea.addEventListener('input', () => {
localStorage.setItem(`aiPrompt_${module}`, textarea.value);
});
}
});
}
/**
* 重置 prompt 為預設值
*/
export function resetPromptToDefault(module) {
const textarea = document.getElementById(`prompt_${module}`);
if (textarea && DEFAULT_AI_PROMPTS[module]) {
textarea.value = DEFAULT_AI_PROMPTS[module];
localStorage.removeItem(`aiPrompt_${module}`);
if (typeof showToast === 'function') {
showToast('已重置為預設 Prompt');
}
}
}
/**
* 執行 AI 幫我想
*/
export async function executeAIHelper(module) {
const container = document.querySelector(`.ai-helper-container[data-module="${module}"]`);
const btn = container.querySelector('.ai-helper-btn');
const textarea = document.getElementById(`prompt_${module}`);
if (!textarea || !textarea.value.trim()) {
if (typeof showToast === 'function') {
showToast('請輸入 Prompt 指令', 'error');
}
return;
}
const originalHTML = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<div class="spinner"></div><span>AI 生成中...</span>';
try {
const prompt = textarea.value.trim();
const data = await callClaudeAPI(prompt);
fillFormWithAIHelperResult(module, data);
if (typeof showToast === 'function') {
showToast('AI 生成完成!已填入表單');
}
} catch (error) {
console.error('AI Helper error:', error);
} finally {
btn.disabled = false;
btn.innerHTML = originalHTML;
}
}
/**
* 根據模組填入 AI 結果
*/
export function fillFormWithAIHelperResult(module, data) {
const fieldMappings = {
positionBasic: {
positionCode: 'positionCode',
positionName: 'positionName',
positionCategory: 'positionCategory',
positionNature: 'positionNature',
headcount: 'headcount',
positionLevel: 'positionLevel',
positionDesc: 'positionDesc',
positionRemark: 'positionRemark'
},
positionRecruit: {
education: 'recruitEducation',
experience: 'recruitExperience',
skills: 'recruitSkills',
certificates: 'recruitCertificates',
languages: 'recruitLanguages',
specialRequirements: 'recruitSpecialReq'
},
jobBasic: {
jobCode: 'jobCode',
jobName: 'jobName',
jobGrade: 'jobGrade',
jobCategory: 'jobCategory',
careerPath: 'careerPath',
jobDesc: 'jobDesc'
},
deptFunction: {
deptCode: 'deptCode',
deptName: 'deptName',
parentDept: 'parentDept',
deptHead: 'deptHead',
deptFunction: 'deptFunction',
kpi: 'kpi'
},
jobDesc: {
positionName: 'descPositionName',
department: 'descDepartment',
directSupervisor: 'descDirectSupervisor',
positionPurpose: 'descPositionPurpose',
mainResponsibilities: 'descMainResponsibilities',
education: 'descEducation',
basicSkills: 'descBasicSkills',
professionalKnowledge: 'descProfessionalKnowledge'
}
};
const mapping = fieldMappings[module];
if (!mapping) return;
let filledCount = 0;
for (const [dataKey, elementId] of Object.entries(mapping)) {
if (data[dataKey] !== undefined) {
const value = Array.isArray(data[dataKey])
? data[dataKey].join('\n')
: String(data[dataKey]);
if (fillIfEmpty(elementId, value)) {
filledCount++;
}
}
}
if (filledCount > 0) {
console.log(`[AI Helper] 已填入 ${filledCount} 個欄位`);
}
}
// ==================== 錯誤顯示 ====================
/**
* 顯示可複製的錯誤訊息
*/
export 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;
">
<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;">${sanitizeHTML(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;
">×</button>
</div>
<div style="padding: 25px; overflow-y: auto; flex: 1;">
<div style="color: #333; line-height: 1.6; margin-bottom: 20px;">${sanitizeHTML(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;">${sanitizeHTML(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;">🔍 詳細錯誤訊息(點擊展開)</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;">${sanitizeHTML(details)}</pre>
</details>
` : ''}
</div>
<div style="padding: 15px 25px; border-top: 1px solid #f0f0f0; display: flex; justify-content: flex-end;">
<button onclick="this.closest('[style*=\"position: fixed\"]').remove()" style="
background: #007bff;
color: white;
border: none;
padding: 10px 25px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
">確定</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
}
// ==================== 各表單 AI 生成函數 ====================
/**
* 生成崗位基礎資料
*/
export async function generatePositionBasic(event) {
const btn = event.target.closest('.ai-generate-btn');
const allFields = ['positionCode', 'positionName', 'positionCategory', 'positionNature', 'headcount', 'positionLevel', 'positionDesc', 'positionRemark'];
const emptyFields = getEmptyFields(allFields);
if (emptyFields.length === 0) {
if (typeof showToast === 'function') showToast('所有欄位都已填寫完成!');
return;
}
setButtonLoading(btn, true);
try {
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 = `你是專業人資顧問,熟悉半導體製造業的人資所有流程。請生成崗位基礎資料。請用繁體中文回覆。
${contextInfo}
請「只生成」以下這些尚未填寫的欄位:${emptyFields.join(', ')}
欄位說明:
- positionCode: 崗位編號(格式如 ENG-001, MGR-002, SAL-003
- positionName: 崗位名稱
- positionCategory: 崗位類別代碼01=技術職, 02=管理職, 03=業務職, 04=行政職)
- positionNature: 崗位性質代碼FT=全職, PT=兼職, CT=約聘, IN=實習)
- headcount: 編制人數1-10之間的數字字串
- positionLevel: 崗位級別L1到L7
- positionDesc: 崗位描述(條列式,用換行分隔)
- positionRemark: 崗位備注(條列式,用換行分隔)
重要請「只」返回純JSON格式不要有任何解釋、思考過程或額外文字。格式如下
{
${emptyFields.map(f => `"${f}": "..."`).join(',\n ')}
}`;
const data = await callClaudeAPI(prompt);
let filledCount = 0;
if (fillIfEmpty('positionCode', data.positionCode)) filledCount++;
if (fillIfEmpty('positionName', data.positionName)) filledCount++;
if (fillIfEmpty('positionCategory', data.positionCategory)) {
filledCount++;
if (typeof updateCategoryName === 'function') updateCategoryName();
}
if (fillIfEmpty('positionNature', data.positionNature)) {
filledCount++;
if (typeof updateNatureName === 'function') updateNatureName();
}
if (fillIfEmpty('headcount', data.headcount)) filledCount++;
if (fillIfEmpty('positionLevel', data.positionLevel)) filledCount++;
if (fillIfEmpty('positionDesc', data.positionDesc)) filledCount++;
if (fillIfEmpty('positionRemark', data.positionRemark)) filledCount++;
if (typeof updatePreview === 'function') updatePreview();
if (typeof showToast === 'function') showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
} catch (error) {
if (typeof showToast === 'function') showToast('生成失敗,請稍後再試');
} finally {
setButtonLoading(btn, false);
}
}
/**
* 生成招聘要求資料
*/
export async function generatePositionRecruit(event) {
const btn = event.target.closest('.ai-generate-btn');
const allFields = ['minEducation', 'requiredGender', 'salaryRange', 'workExperience', 'minAge', 'maxAge', 'jobType', 'recruitPosition', 'jobTitle', 'jobDesc', 'positionReq', 'skillReq', 'langReq', 'otherReq'];
const emptyFields = getEmptyFields(allFields);
if (emptyFields.length === 0) {
if (typeof showToast === 'function') showToast('所有欄位都已填寫完成!');
return;
}
setButtonLoading(btn, true);
try {
const positionName = getFieldValue('positionName') || '一般職位';
const existingData = { positionName };
allFields.forEach(field => {
const value = getFieldValue(field);
if (value) existingData[field] = value;
});
const prompt = `請生成「${positionName}」的招聘要求資料。請用繁體中文回覆。
已填寫的資料(請參考這些內容來生成相關的資料):
${JSON.stringify(existingData, null, 2)}
請「只生成」以下這些尚未填寫的欄位:${emptyFields.join(', ')}
欄位說明:
- minEducation: 最低學歷代碼HS=高中職, JC=專科, BA=大學, MA=碩士, PHD=博士)
- requiredGender: 要求性別(空字串=不限, M=男, F=女)
- salaryRange: 薪酬范圍代碼A=30000以下, B=30000-50000, C=50000-80000, D=80000-120000, E=120000以上, N=面議)
- workExperience: 工作經驗年數0=不限, 1, 3, 5, 10
- minAge: 最小年齡18-30之間的數字字串
- maxAge: 最大年齡35-55之間的數字字串
- jobType: 工作性質代碼FT=全職, PT=兼職, CT=約聘, DP=派遣)
- recruitPosition: 招聘職位代碼ENG=工程師, MGR=經理, AST=助理, OP=作業員, SAL=業務)
- jobTitle: 職位名稱
- jobDesc: 職位描述2-3句話
- positionReq: 崗位要求(條列式,用換行分隔)
- skillReq: 技能要求(條列式,用換行分隔)
- langReq: 語言要求(條列式,用換行分隔)
- otherReq: 其他要求(條列式,用換行分隔)
重要請「只」返回純JSON格式不要有任何解釋、思考過程或額外文字。格式如下
{
${emptyFields.map(f => `"${f}": "..."`).join(',\n ')}
}`;
const data = await callClaudeAPI(prompt);
let filledCount = 0;
if (fillIfEmpty('minEducation', data.minEducation)) filledCount++;
if (fillIfEmpty('requiredGender', data.requiredGender)) filledCount++;
if (fillIfEmpty('salaryRange', data.salaryRange)) filledCount++;
if (fillIfEmpty('workExperience', data.workExperience)) filledCount++;
if (fillIfEmpty('minAge', data.minAge)) filledCount++;
if (fillIfEmpty('maxAge', data.maxAge)) filledCount++;
if (fillIfEmpty('jobType', data.jobType)) filledCount++;
if (fillIfEmpty('recruitPosition', data.recruitPosition)) filledCount++;
if (fillIfEmpty('jobTitle', data.jobTitle)) filledCount++;
if (fillIfEmpty('jobDesc', data.jobDesc)) filledCount++;
if (fillIfEmpty('positionReq', data.positionReq)) filledCount++;
if (fillIfEmpty('skillReq', data.skillReq)) filledCount++;
if (fillIfEmpty('langReq', data.langReq)) filledCount++;
if (fillIfEmpty('otherReq', data.otherReq)) filledCount++;
if (typeof updatePreview === 'function') updatePreview();
if (typeof showToast === 'function') showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
} catch (error) {
if (typeof showToast === 'function') showToast('生成失敗,請稍後再試');
} finally {
setButtonLoading(btn, false);
}
}
/**
* 生成職務基礎資料
*/
export async function generateJobBasic(event) {
const btn = event.target.closest('.ai-generate-btn');
const allFields = ['jobCategoryCode', 'jobCode', 'jobName', 'jobNameEn', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel'];
const emptyFields = getEmptyFields(allFields);
const attBonusChk = document.getElementById('job_hasAttBonus');
const houseAllowChk = document.getElementById('job_hasHouseAllow');
const needCheckboxes = !(attBonusChk?.checked) && !(houseAllowChk?.checked);
if (emptyFields.length === 0 && !needCheckboxes) {
if (typeof showToast === 'function') showToast('所有欄位都已填寫完成!');
return;
}
setButtonLoading(btn, true);
try {
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 fieldsToGenerate = [...emptyFields];
if (needCheckboxes) {
fieldsToGenerate.push('hasAttendanceBonus', 'hasHousingAllowance');
}
const prompt = `你是專業人資顧問,熟悉半導體製造業的人資所有流程。請生成職務基礎資料。請用繁體中文回覆。
${contextInfo}
請「只生成」以下這些尚未填寫的欄位:${fieldsToGenerate.join(', ')}
欄位說明:
- jobCategoryCode: 職務類別代碼MGR=管理職, TECH=技術職, SALE=業務職, ADMIN=行政職, RD=研發職, PROD=生產職)
- jobCode: 職務編號(格式如 MGR-001, TECH-002
- jobName: 職務名稱
- jobNameEn: 職務英文名稱
- jobHeadcount: 編制人數1-20之間的數字字串
- jobSortOrder: 排列順序10, 20, 30...的數字字串)
- jobRemark: 備注說明
- jobLevel: 職務層級(可以是 *保密* 或具體層級)
- hasAttendanceBonus: 是否有全勤true/false
- hasHousingAllowance: 是否住房補貼true/false
重要請「只」返回純JSON格式不要有任何解釋、思考過程或額外文字。格式如下
{
${fieldsToGenerate.map(f => `"${f}": "..."`).join(',\n ')}
}`;
const data = await callClaudeAPI(prompt);
let filledCount = 0;
if (fillIfEmpty('jobCategoryCode', data.jobCategoryCode)) {
filledCount++;
if (typeof updateJobCategoryName === 'function') updateJobCategoryName();
}
if (fillIfEmpty('jobCode', data.jobCode)) filledCount++;
if (fillIfEmpty('jobName', data.jobName)) filledCount++;
if (fillIfEmpty('jobNameEn', data.jobNameEn)) filledCount++;
if (fillIfEmpty('jobHeadcount', data.jobHeadcount)) filledCount++;
if (fillIfEmpty('jobSortOrder', data.jobSortOrder)) filledCount++;
if (fillIfEmpty('jobRemark', data.jobRemark)) filledCount++;
if (fillIfEmpty('jobLevel', data.jobLevel)) filledCount++;
if (needCheckboxes) {
const attendanceCheckbox = document.getElementById('job_hasAttBonus');
const housingCheckbox = document.getElementById('job_hasHouseAllow');
if (data.hasAttendanceBonus === true && attendanceCheckbox) {
attendanceCheckbox.checked = true;
document.getElementById('attendanceLabel').textContent = '是';
filledCount++;
}
if (data.hasHousingAllowance === true && housingCheckbox) {
housingCheckbox.checked = true;
document.getElementById('housingLabel').textContent = '是';
filledCount++;
}
}
if (typeof updatePreview === 'function') updatePreview();
if (typeof showToast === 'function') showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
} catch (error) {
if (typeof showToast === 'function') showToast('生成失敗,請稍後再試');
} finally {
setButtonLoading(btn, false);
}
}
/**
* 生成崗位描述資料
*/
export async function generateJobDesc(event) {
const btn = event.target.closest('.ai-generate-btn');
const allFields = ['jd_empNo', 'jd_empName', 'jd_positionCode', 'jd_versionDate', 'jd_positionName', 'jd_department', 'jd_positionEffectiveDate', 'jd_directSupervisor', 'jd_directReports', 'jd_workLocation', 'jd_empAttribute', 'jd_positionPurpose', 'jd_mainResponsibilities', 'jd_education', 'jd_basicSkills', 'jd_professionalKnowledge', 'jd_workExperienceReq', 'jd_otherRequirements'];
const emptyFields = allFields.filter(id => {
const el = document.getElementById(id);
const value = el ? el.value.trim() : '';
if (id === 'jd_mainResponsibilities') {
return !value || value === '1、\n2、\n3、\n4、' || value === '1、\n2、\n3、\n4、\n5、';
}
return !value;
});
if (emptyFields.length === 0) {
if (typeof showToast === 'function') showToast('所有欄位都已填寫完成!');
return;
}
setButtonLoading(btn, true);
try {
const existingData = {};
allFields.forEach(field => {
const el = document.getElementById(field);
const value = el ? el.value.trim() : '';
if (value && value !== '1、\n2、\n3、\n4、') {
existingData[field.replace('jd_', '')] = value;
}
});
const contextInfo = Object.keys(existingData).length > 0
? `\n\n已填寫的資料(請參考這些內容來生成相關的資料):\n${JSON.stringify(existingData, null, 2)}`
: '';
const fieldsToGenerate = emptyFields.map(f => f.replace('jd_', ''));
const prompt = `請生成崗位描述資料。請用繁體中文回覆。
${contextInfo}
請「只生成」以下這些尚未填寫的欄位:${fieldsToGenerate.join(', ')}
欄位說明:
- empNo: 工號(格式如 A001234
- empName: 員工姓名
- positionCode: 崗位代碼
- versionDate: 版本日期YYYY-MM-DD格式
- positionName: 崗位名稱
- department: 所屬部門
- positionEffectiveDate: 崗位生效日期YYYY-MM-DD格式
- directSupervisor: 直接領導職務
- directReports: 直接下級(格式如「工程師 x 5人」
- workLocation: 任職地點代碼HQ=總部, TPE=台北, TYC=桃園, KHH=高雄, SH=上海, SZ=深圳)
- empAttribute: 員工屬性代碼FT=正式員工, CT=約聘, PT=兼職, IN=實習, DP=派遣)
- positionPurpose: 崗位設置目的1句話說明
- mainResponsibilities: 主要崗位職責用「1、」「2、」「3、」「4、」「5、」格式每項換行\\n分隔
- education: 教育程度要求(條列式,用換行分隔)
- basicSkills: 基本技能要求(條列式,用換行分隔)
- professionalKnowledge: 專業知識要求(條列式,用換行分隔)
- workExperienceReq: 工作經驗要求(條列式,用換行分隔)
- otherRequirements: 其他要求(條列式,用換行分隔)
重要請「只」返回純JSON格式不要有任何解釋、思考過程或額外文字。格式如下
{
${fieldsToGenerate.map(f => `"${f}": "..."`).join(',\n ')}
}`;
const data = await callClaudeAPI(prompt);
let filledCount = 0;
const fieldMapping = {
'empNo': 'jd_empNo',
'empName': 'jd_empName',
'positionCode': 'jd_positionCode',
'versionDate': 'jd_versionDate',
'positionName': 'jd_positionName',
'department': 'jd_department',
'positionEffectiveDate': 'jd_positionEffectiveDate',
'directSupervisor': 'jd_directSupervisor',
'directReports': 'jd_directReports',
'workLocation': 'jd_workLocation',
'empAttribute': 'jd_empAttribute',
'positionPurpose': 'jd_positionPurpose',
'mainResponsibilities': 'jd_mainResponsibilities',
'education': 'jd_education',
'basicSkills': 'jd_basicSkills',
'professionalKnowledge': 'jd_professionalKnowledge',
'workExperienceReq': 'jd_workExperienceReq',
'otherRequirements': 'jd_otherRequirements'
};
Object.keys(fieldMapping).forEach(apiField => {
const htmlId = fieldMapping[apiField];
if (data[apiField]) {
const el = document.getElementById(htmlId);
const currentValue = el ? el.value.trim() : '';
const isEmpty = !currentValue || currentValue === '1、\n2、\n3、\n4、';
if (isEmpty) {
el.value = data[apiField];
filledCount++;
}
}
});
if (typeof updatePreview === 'function') updatePreview();
if (typeof showToast === 'function') showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
} catch (error) {
if (typeof showToast === 'function') showToast('生成失敗,請稍後再試');
} finally {
setButtonLoading(btn, false);
}
}

308
js/api.js Normal file
View File

@@ -0,0 +1,308 @@
/**
* API - API 呼叫函式
* 包含所有與後端 API 的通訊邏輯
*/
import { API_BASE_URL } from './config.js';
import { showCopyableError } from './utils.js';
// ==================== LLM API ====================
/**
* 獲取當前選擇的 Ollama 模型
* @returns {string} - 模型名稱
*/
function getOllamaModel() {
return localStorage.getItem('selectedOllamaModel') || 'deepseek-r1:8b';
}
/**
* 調用 Claude/Ollama LLM API
* @param {string} prompt - Prompt 內容
* @param {string} api - API 類型('ollama' 或其他)
* @returns {Promise<Object>} - 解析後的 JSON 回應
*/
export async function callClaudeAPI(prompt, api = 'ollama') {
try {
// 準備請求資料
const requestData = {
api: api,
prompt: prompt,
max_tokens: 2000
};
// 如果使用 Ollama API加入選擇的模型
if (api === 'ollama') {
requestData.model = getOllamaModel();
}
// 調用後端 Flask API避免 CORS 錯誤
const response = await fetch(`${API_BASE_URL}/llm/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
});
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;
// 移除 DeepSeek-R1 等模型的思考標籤 <think>...</think>
responseText = responseText.replace(/<think>[\s\S]*?<\/think>/gi, '');
// 移除 Markdown 代碼塊標記
responseText = responseText.replace(/```json\n?/gi, '').replace(/```\n?/g, '').trim();
// 移除可能的前導文字(如 "Here is the JSON:" 等)
responseText = responseText.replace(/^[\s\S]*?(?=\{)/i, '');
// 嘗試提取 JSON 內容(如果包含其他文字)
// 查找第一個 { 和最後一個 }
const firstBrace = responseText.indexOf('{');
const lastBrace = responseText.lastIndexOf('}');
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
responseText = responseText.substring(firstBrace, lastBrace + 1);
}
// 嘗試解析 JSON
try {
return JSON.parse(responseText);
} catch (parseError) {
// JSON 解析失敗,拋出更詳細的錯誤
console.error('JSON 解析失敗,原始響應:', responseText);
// 嘗試修復常見的 JSON 問題
try {
// 移除控制字符
const cleanedText = responseText
.replace(/[\x00-\x1F\x7F]/g, '') // 移除控制字符
.replace(/,\s*}/g, '}') // 移除末尾逗號
.replace(/,\s*]/g, ']'); // 移除陣列末尾逗號
return JSON.parse(cleanedText);
} catch (retryError) {
throw new Error(`LLM 返回的內容不是有效的 JSON 格式。\n\n原始響應前 300 字符:\n${responseText.substring(0, 300)}...`);
}
}
} catch (error) {
console.error('Error calling LLM API:', error);
// 嘗試解析更詳細的錯誤訊息
let errorDetails = error.message;
try {
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 app.py)',
'已在 .env 文件中配置有效的 LLM API Key',
'網路連線正常',
'確認 Prompt 要求返回純 JSON 格式',
'嘗試使用不同的 LLM API (切換到其他模型)',
'檢查 LLM 模型是否支援繁體中文'
]
});
throw error;
}
}
// ==================== Position API ====================
/**
* 保存崗位至崗位清單
* @param {Object} positionData - 崗位資料
* @returns {Promise<Object>} - API 回應
*/
export async function savePositionToList(positionData) {
const response = await fetch(`${API_BASE_URL}/positions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(positionData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '保存失敗');
}
return await response.json();
}
/**
* 載入崗位清單
* @returns {Promise<Array>} - 崗位清單
*/
export async function loadPositionList() {
const response = await fetch(`${API_BASE_URL}/position-list`);
if (!response.ok) {
throw new Error('載入崗位清單失敗');
}
const data = await response.json();
return data.data || [];
}
/**
* 獲取單一崗位描述
* @param {string} positionCode - 崗位編號
* @returns {Promise<Object>} - 崗位描述資料
*/
export async function getPositionDescription(positionCode) {
const response = await fetch(`${API_BASE_URL}/position-descriptions/${positionCode}`);
if (!response.ok) {
if (response.status === 404) {
return null; // 未找到描述
}
throw new Error('載入崗位描述失敗');
}
const data = await response.json();
return data.data;
}
/**
* 保存崗位描述
* @param {Object} descData - 崗位描述資料
* @returns {Promise<Object>} - API 回應
*/
export async function savePositionDescription(descData) {
const response = await fetch(`${API_BASE_URL}/position-descriptions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(descData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '保存崗位描述失敗');
}
return await response.json();
}
// ==================== CSV API ====================
/**
* 下載崗位 CSV 範本
*/
export async function downloadPositionCSVTemplate() {
window.location.href = `${API_BASE_URL}/positions/csv-template`;
}
/**
* 下載職務 CSV 範本
*/
export async function downloadJobCSVTemplate() {
window.location.href = `${API_BASE_URL}/jobs/csv-template`;
}
/**
* 匯入崗位 CSV
* @param {File} file - CSV 檔案
* @returns {Promise<Object>} - API 回應
*/
export async function importPositionsCSV(file) {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${API_BASE_URL}/positions/import-csv`, {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'CSV 匯入失敗');
}
return await response.json();
}
/**
* 匯入職務 CSV
* @param {File} file - CSV 檔案
* @returns {Promise<Object>} - API 回應
*/
export async function importJobsCSV(file) {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${API_BASE_URL}/jobs/import-csv`, {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'CSV 匯入失敗');
}
return await response.json();
}
/**
* 匯出完整崗位資料
*/
export async function exportCompletePositionData() {
window.location.href = `${API_BASE_URL}/position-list/export`;
}
// ==================== Ollama Connection Test ====================
/**
* 測試 Ollama 連線
* @returns {Promise<boolean>} - 連線是否成功
*/
export async function testOllamaConnection() {
try {
const response = await fetch(`${API_BASE_URL}/llm/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
api: 'ollama',
model: getOllamaModel(),
prompt: '請回答:「連線測試成功」',
max_tokens: 50
})
});
if (!response.ok) {
return false;
}
const data = await response.json();
return data.success === true;
} catch (error) {
console.error('Ollama 連線測試失敗:', error);
return false;
}
}

82
js/config.js Normal file
View File

@@ -0,0 +1,82 @@
/**
* Configuration - 設定檔
* 包含 API 端點、常數定義、資料對應表
*/
// ==================== API Configuration ====================
export const API_BASE_URL = '/api';
// ==================== 下拉選單資料 (從 Excel 提取) ====================
// 事業體
export const businessUnits = [
'半導體事業群', '汽車事業體', '法務室', '岡山製造事業體', '產品事業體',
'晶圓三廠', '集團人資行政事業體', '集團財務事業體', '集團會計事業體',
'集團資訊事業體', '新創事業體', '稽核室', '總經理室', '總品質事業體', '營業事業體'
];
// 處級單位
export const deptLevel1Units = [
'半導體事業群', '汽車事業體', '法務室', '生產處', '岡山製造事業體', '封裝工程處',
'副總辦公室', '測試工程與研發處', '資材處', '廠務與環安衛管理處', '產品事業體',
'先進產品事業處', '成熟產品事業處', '晶圓三廠', '製程工程處', '集團人資行政事業體',
'集團財務事業體', '岡山強茂財務處', '集團會計事業體', '岡山會計處', '集團會計處',
'集團資訊事業體', '資安行動小組', '資訊一處', '資訊二處', '新創事業體',
'中低壓產品研發處', '研發中心', '高壓產品研發處', '稽核室', '總經理室',
'ESG專案辦公室', '專案管理室', '總品質事業體', '營業事業體', '商業開發暨市場應用處',
'海外銷售事業處', '全球技術服務處', '全球行銷暨業務支援處', '大中華區銷售事業處'
];
// 部級單位
export const deptLevel2Units = [
'生產部', '生產企劃部', '岡山品質管制部', '製程工程一部', '製程工程二部', '設備一部',
'設備二部', '工業工程部', '測試工程部', '新產品導入部', '研發部', '採購部',
'外部資源部', '生管部', '原物料控制部', '廠務部', '產品管理部(APD)', '產品管理部(MPD)',
'品質部', '製造部', '廠務部(Fab3)', '工程一部', '工程二部', '工程三部',
'製程整合部(Fab3)', '行政總務管理部', '招募任用部', '訓練發展部', '薪酬管理部',
'岡山強茂財務部', '會計部', '管理會計部', '集團合併報表部', '應用系統部',
'電腦整合製造部', '系統網路服務部', '資源管理部', '客戶品質管理部', '產品品質管理部',
'品質系統及客戶工程整合部', '封測外包品質管理部', '品質保證部', '日本區暨代工業務部',
'歐亞區業務部', '韓國區業務部-韓國區', '美洲區業務部', '應用工程部(GTS)', '系統工程部',
'特性測試部', '業務生管部', '市場行銷企劃部', 'MOSFET晶圓採購部', '台灣區業務部',
'業務一部', '業務二部'
];
// 崗位名稱
export const positionNames = [
'營運長', '營運長助理', '副總經理', '專案經理', '經副理', '法務專員', '專利工程師',
'處長', '專員', '課長', '組長', '班長', '副班長', '作業員', '工程師', '副總經理助理',
'副理', '專案經副理', '顧問', '人資長', '助理', '財務長', '專案副理', '會計長',
'資訊長', '主任', '總裁', '總經理', '專員/工程師', '經理', '技術經副理', '處長/資深經理'
];
// ==================== 資料對應表 ====================
// 崗位類別代碼對應中文名稱
export const categoryMap = {
'01': '技術職',
'02': '管理職',
'03': '業務職',
'04': '行政職'
};
// 崗位性質代碼對應中文名稱
export const natureMap = {
'FT': '全職',
'PT': '兼職',
'CT': '約聘',
'IN': '實習'
};
// 職務類別代碼對應中文名稱
export const jobCategoryMap = {
'MGR': '管理職',
'TECH': '技術職',
'SALE': '業務職',
'ADMIN': '行政職',
'RD': '研發職',
'PROD': '生產職'
};
// ==================== Toast 持續時間 ====================
export const TOAST_DURATION = 3000;

337
js/csv.js Normal file
View File

@@ -0,0 +1,337 @@
/**
* CSV 匯入匯出模組
* 處理各表單的 CSV 資料交換
*/
const API_BASE_URL = '/api';
// ==================== CSV 工具函數 ====================
export const CSVUtils = {
/**
* 匯出資料到 CSV
*/
exportToCSV(data, filename, headers) {
if (!data || data.length === 0) {
console.warn('No data to export');
return;
}
const csvHeaders = headers || Object.keys(data[0]);
const csvRows = data.map(row => {
return csvHeaders.map(header => {
let value = row[header] !== undefined ? row[header] : '';
// 處理包含逗號或換行的值
if (typeof value === 'string' && (value.includes(',') || value.includes('\n') || value.includes('"'))) {
value = '"' + value.replace(/"/g, '""') + '"';
}
return value;
}).join(',');
});
const csvContent = '\uFEFF' + [csvHeaders.join(','), ...csvRows].join('\n'); // BOM for UTF-8
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
},
/**
* 從 CSV 匯入資料
*/
importFromCSV(file, callback) {
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target.result;
const lines = text.split('\n').filter(line => line.trim());
if (lines.length < 2) {
callback([]);
return;
}
const headers = this.parseCSVLine(lines[0]);
const data = [];
for (let i = 1; i < lines.length; i++) {
const values = this.parseCSVLine(lines[i]);
const row = {};
headers.forEach((header, index) => {
row[header.trim()] = values[index] ? values[index].trim() : '';
});
data.push(row);
}
callback(data);
};
reader.readAsText(file, 'UTF-8');
},
/**
* 解析 CSV 行(處理引號內的逗號)
*/
parseCSVLine(line) {
const result = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
if (inQuotes && line[i + 1] === '"') {
current += '"';
i++;
} else {
inQuotes = !inQuotes;
}
} else if (char === ',' && !inQuotes) {
result.push(current);
current = '';
} else {
current += char;
}
}
result.push(current);
return result;
}
};
// ==================== 崗位資料 CSV ====================
export function downloadPositionCSVTemplate() {
window.location.href = API_BASE_URL + '/positions/csv-template';
if (typeof showToast === 'function') showToast('正在下載崗位資料範本...');
}
export 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);
if (typeof showToast === 'function') showToast('崗位資料已匯出!');
}
export function importPositionsCSV() {
document.getElementById('positionCSVInput').click();
}
export function handlePositionCSVImport(event) {
const file = event.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
if (typeof showToast === 'function') showToast('正在匯入崗位資料...');
fetch(API_BASE_URL + '/positions/import-csv', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
let message = data.message;
if (data.errors && data.errors.length > 0) {
message += '\n\n錯誤詳情:\n' + data.errors.join('\n');
}
alert(message);
} else {
alert('匯入失敗: ' + (data.error || '未知錯誤'));
}
})
.catch(error => {
console.error('匯入錯誤:', error);
alert('匯入失敗: ' + error.message);
})
.finally(() => {
event.target.value = '';
});
}
// ==================== 職務資料 CSV ====================
export function downloadJobCSVTemplate() {
window.location.href = API_BASE_URL + '/jobs/csv-template';
if (typeof showToast === 'function') showToast('正在下載職務資料範本...');
}
export 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('job_hasAttBonus')?.checked,
hasHousingAllowance: document.getElementById('job_hasHouseAllow')?.checked
}];
const headers = ['jobCategoryCode', 'jobCategoryName', 'jobCode', 'jobName', 'jobNameEn',
'jobEffectiveDate', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel',
'hasAttendanceBonus', 'hasHousingAllowance'];
CSVUtils.exportToCSV(data, 'jobs.csv', headers);
if (typeof showToast === 'function') showToast('職務資料已匯出!');
}
export function importJobsCSV() {
document.getElementById('jobCSVInput').click();
}
export function handleJobCSVImport(event) {
const file = event.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
if (typeof showToast === 'function') showToast('正在匯入職務資料...');
fetch(API_BASE_URL + '/jobs/import-csv', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
let message = data.message;
if (data.errors && data.errors.length > 0) {
message += '\n\n錯誤詳情:\n' + data.errors.join('\n');
}
alert(message);
} else {
alert('匯入失敗: ' + (data.error || '未知錯誤'));
}
})
.catch(error => {
console.error('匯入錯誤:', error);
alert('匯入失敗: ' + error.message);
})
.finally(() => {
event.target.value = '';
});
}
// ==================== 崗位描述 CSV ====================
export 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);
if (typeof showToast === 'function') showToast('崗位描述已匯出!');
}
export function importDescriptionsCSV() {
document.getElementById('descCSVInput').click();
}
export 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];
}
});
if (typeof showToast === 'function') {
showToast(`已匯入 ${data.length} 筆崗位描述資料(顯示第一筆)`);
}
}
});
event.target.value = '';
}
// ==================== 崗位清單 CSV ====================
export function exportPositionListCSV(positionListData) {
if (!positionListData || positionListData.length === 0) {
if (typeof showToast === 'function') showToast('請先載入清單資料');
return;
}
const headers = ['positionCode', 'positionName', 'businessUnit', 'department', 'positionCategory', 'headcount', 'effectiveDate'];
CSVUtils.exportToCSV(positionListData, 'position_list.csv', headers);
if (typeof showToast === 'function') showToast('崗位清單已匯出!');
}
// ==================== 部門職責 CSV ====================
export function importDeptFunctionCSV() {
document.getElementById('deptFunctionCsvInput').click();
}
export function handleDeptFunctionCSVImport(event, callback) {
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];
});
if (typeof showToast === 'function') showToast('已匯入 CSV 資料!');
if (callback) callback(data);
}
});
event.target.value = '';
}
export function exportDeptFunctionCSV(formData) {
const headers = Object.keys(formData);
CSVUtils.exportToCSV([formData], 'dept_function.csv', headers);
if (typeof showToast === 'function') showToast('部門職責資料已匯出!');
}
// ==================== 工具函數 ====================
function getFieldValue(elementId) {
const el = document.getElementById(elementId);
return el ? el.value.trim() : '';
}
// 暴露到全域
if (typeof window !== 'undefined') {
window.CSVUtils = CSVUtils;
}

238
js/data/hierarchy.js Normal file
View File

@@ -0,0 +1,238 @@
/**
* 組織階層靜態資料模組
* 從 Excel 提取的下拉選單資料
*/
// ==================== 下拉選單資料 ====================
// 事業體
export const businessUnits = [
'半導體事業群', '汽車事業體', '法務室', '岡山製造事業體', '產品事業體',
'晶圓三廠', '集團人資行政事業體', '集團財務事業體', '集團會計事業體',
'集團資訊事業體', '新創事業體', '稽核室', '總經理室', '總品質事業體', '營業事業體'
];
// 處級單位
export const deptLevel1Units = [
'半導體事業群', '汽車事業體', '法務室', '生產處', '岡山製造事業體',
'封裝工程處', '副總辦公室', '測試工程與研發處', '資材處', '廠務與環安衛管理處',
'產品事業體', '先進產品事業處', '成熟產品事業處', '晶圓三廠', '製程工程處',
'集團人資行政事業體', '集團財務事業體', '岡山強茂財務處', '集團會計事業體',
'岡山會計處', '集團會計處', '集團資訊事業體', '資安行動小組', '資訊一處',
'資訊二處', '新創事業體', '中低壓產品研發處', '研發中心', '高壓產品研發處',
'稽核室', '總經理室', 'ESG專案辦公室', '專案管理室', '總品質事業體',
'營業事業體', '商業開發暨市場應用處', '海外銷售事業處', '全球技術服務處',
'全球行銷暨業務支援處', '大中華區銷售事業處'
];
// 部級單位
export const deptLevel2Units = [
'生產部', '生產企劃部', '岡山品質管制部', '製程工程一部', '製程工程二部',
'設備一部', '設備二部', '工業工程部', '測試工程部', '新產品導入部',
'研發部', '採購部', '外部資源部', '生管部', '原物料控制部', '廠務部',
'產品管理部(APD)', '產品管理部(MPD)', '品質部', '製造部', '廠務部(Fab3)',
'工程一部', '工程二部', '工程三部', '製程整合部(Fab3)', '行政總務管理部',
'招募任用部', '訓練發展部', '薪酬管理部', '岡山強茂財務部', '會計部',
'管理會計部', '集團合併報表部', '應用系統部', '電腦整合製造部', '系統網路服務部',
'資源管理部', '客戶品質管理部', '產品品質管理部', '品質系統及客戶工程整合部',
'封測外包品質管理部', '品質保證部', '日本區暨代工業務部', '歐亞區業務部',
'韓國區業務部-韓國區', '美洲區業務部', '應用工程部(GTS)', '系統工程部',
'特性測試部', '業務生管部', '市場行銷企劃部', 'MOSFET晶圓採購部',
'台灣區業務部', '業務一部', '業務二部'
];
// 崗位名稱
export const positionNames = [
'營運長', '營運長助理', '副總經理', '專案經理', '經副理', '法務專員',
'專利工程師', '處長', '專員', '課長', '組長', '班長', '副班長', '作業員',
'工程師', '副總經理助理', '副理', '專案經副理', '顧問', '人資長', '助理',
'財務長', '專案副理', '會計長', '資訊長', '主任', '總裁', '總經理',
'專員/工程師', '經理', '技術經副理', '處長/資深經理'
];
// ==================== 組織階層級聯映射 ====================
// 事業體 -> 處級單位 (預設資料,可被 API 覆蓋)
export let businessToDivision = {
'半導體事業群': ['半導體事業群'],
'汽車事業體': ['汽車事業體'],
'法務室': ['法務室'],
'岡山製造事業體': ['生產處', '岡山製造事業體', '封裝工程處', '副總辦公室', '測試工程與研發處', '資材處', '廠務與環安衛管理處'],
'產品事業體': ['產品事業體', '先進產品事業處', '成熟產品事業處'],
'晶圓三廠': ['晶圓三廠', '製程工程處'],
'集團人資行政事業體': ['集團人資行政事業體'],
'集團財務事業體': ['集團財務事業體', '岡山強茂財務處'],
'集團會計事業體': ['集團會計事業體', '岡山會計處', '集團會計處'],
'集團資訊事業體': ['集團資訊事業體', '資安行動小組', '資訊一處', '資訊二處'],
'新創事業體': ['新創事業體', '中低壓產品研發處', '研發中心', '高壓產品研發處'],
'稽核室': ['稽核室'],
'總經理室': ['總經理室', 'ESG專案辦公室', '專案管理室'],
'總品質事業體': ['總品質事業體'],
'營業事業體': ['營業事業體', '商業開發暨市場應用處', '海外銷售事業處', '全球技術服務處', '全球行銷暨業務支援處', '大中華區銷售事業處']
};
// 處級單位 -> 部級單位 (預設資料,可被 API 覆蓋)
export let divisionToDepartment = {
'半導體事業群': ['(直屬)'],
'汽車事業體': ['(直屬)'],
'法務室': ['(直屬)'],
'生產處': ['(直屬)', '生產部', '生產企劃部'],
'岡山製造事業體': ['岡山品質管制部'],
'封裝工程處': ['(直屬)', '製程工程一部', '製程工程二部', '設備一部', '設備二部'],
'副總辦公室': ['工業工程部'],
'測試工程與研發處': ['(直屬)', '測試工程部', '新產品導入部', '研發部'],
'資材處': ['(直屬)', '採購部', '外部資源部', '生管部', '原物料控制部'],
'廠務與環安衛管理處': ['(直屬)', '廠務部'],
'產品事業體': ['廠務部'],
'先進產品事業處': ['(直屬)', '產品管理部(APD)'],
'成熟產品事業處': ['(直屬)', '產品管理部(MPD)'],
'晶圓三廠': ['產品管理部(MPD)', '品質部', '製造部', '廠務部(Fab3)'],
'製程工程處': ['工程一部', '工程二部', '工程三部', '製程整合部(Fab3)'],
'集團人資行政事業體': ['製程整合部(Fab3)', '行政總務管理部', '招募任用部', '訓練發展部', '薪酬管理部'],
'集團財務事業體': ['薪酬管理部', '岡山強茂財務部'],
'岡山強茂財務處': ['(直屬)', '岡山強茂財務部'],
'集團會計事業體': ['岡山強茂財務部'],
'岡山會計處': ['(直屬)', '會計部', '管理會計部'],
'集團會計處': ['(直屬)', '集團合併報表部'],
'集團資訊事業體': ['集團合併報表部'],
'資安行動小組': ['集團合併報表部'],
'資訊一處': ['應用系統部', '電腦整合製造部', '系統網路服務部'],
'資訊二處': ['(直屬)'],
'新創事業體': ['(直屬)', '資源管理部'],
'中低壓產品研發處': ['(直屬)'],
'研發中心': ['(直屬)'],
'高壓產品研發處': ['(直屬)'],
'稽核室': ['(直屬)'],
'總經理室': ['(直屬)'],
'ESG專案辦公室': ['(直屬)'],
'專案管理室': ['(直屬)'],
'總品質事業體': ['(直屬)', '客戶品質管理部', '產品品質管理部', '品質系統及客戶工程整合部', '封測外包品質管理部', '品質保證部'],
'營業事業體': ['品質保證部'],
'商業開發暨市場應用處': ['(直屬)'],
'海外銷售事業處': ['(直屬)', '日本區暨代工業務部', '歐亞區業務部', '韓國區業務部-韓國區', '美洲區業務部'],
'全球技術服務處': ['(直屬)', '應用工程部(GTS)', '系統工程部', '特性測試部'],
'全球行銷暨業務支援處': ['(直屬)', '業務生管部', '市場行銷企劃部', 'MOSFET晶圓採購部'],
'大中華區銷售事業處': ['(直屬)', '台灣區業務部', '業務一部', '業務二部']
};
// 部級單位 -> 崗位名稱 (預設資料,可被 API 覆蓋)
export let departmentToPosition = {
'(直屬)': ['營運長', '營運長助理', '副總經理', '專案經理', '經副理', '法務專員', '專利工程師', '處長', '專員', '工程師', '課長', '專員/工程師', '主任', '總裁', '總經理', '經理', '助理'],
'生產部': ['經副理', '課長', '組長', '班長', '副班長', '作業員'],
'生產企劃部': ['經副理', '課長', '專員', '工程師'],
'岡山品質管制部': ['經副理', '課長', '工程師', '組長', '班長', '副班長', '作業員', '副總經理', '副總經理助理'],
'製程工程一部': ['經副理'],
'製程工程二部': ['經副理', '課長', '工程師'],
'設備一部': ['經副理'],
'設備二部': ['經副理', '課長', '工程師'],
'工業工程部': ['經副理', '工程師', '課長', '副理'],
'測試工程部': ['經副理', '課長', '工程師'],
'新產品導入部': ['經副理', '專員', '工程師'],
'研發部': ['經副理', '課長', '工程師', '專員'],
'採購部': ['經副理', '課長', '專員'],
'外部資源部': ['專員'],
'生管部': ['經副理', '課長', '專員', '班長', '副班長', '作業員'],
'原物料控制部': ['經副理', '課長', '專員', '班長', '副班長', '作業員'],
'廠務部': ['經副理', '課長', '工程師', '專員', '處長'],
'產品管理部(APD)': ['經副理', '工程師'],
'產品管理部(MPD)': ['經副理', '專案經副理', '工程師', '顧問', '專員'],
'品質部': ['經副理', '工程師', '作業員'],
'製造部': ['經副理', '課長', '班長', '副班長', '作業員'],
'廠務部(Fab3)': ['經副理', '工程師'],
'工程一部': ['經副理', '工程師'],
'工程二部': ['經副理', '工程師'],
'工程三部': ['經副理', '工程師'],
'製程整合部(Fab3)': ['經副理', '工程師', '人資長'],
'行政總務管理部': ['經副理', '專員', '助理'],
'招募任用部': ['經副理', '專員'],
'訓練發展部': ['經副理', '專員'],
'薪酬管理部': ['經副理', '專員', '財務長'],
'岡山強茂財務部': ['經副理', '課長', '專員', '專案副理', '會計長'],
'會計部': ['經副理', '課長', '專員'],
'管理會計部': ['經副理', '課長', '專員'],
'集團合併報表部': ['經副理', '專員', '資訊長', '課長'],
'應用系統部': ['經副理', '工程師'],
'電腦整合製造部': ['經副理', '工程師'],
'系統網路服務部': ['經副理', '工程師'],
'資源管理部': ['經副理', '專員'],
'客戶品質管理部': ['經副理', '課長', '工程師', '專員'],
'產品品質管理部': ['經副理', '課長', '工程師', '班長', '作業員'],
'品質系統及客戶工程整合部': ['經副理', '課長', '工程師'],
'封測外包品質管理部': ['經副理', '課長', '工程師'],
'品質保證部': ['經副理', '課長', '工程師', '班長', '副班長', '作業員', '副總經理', '副總經理助理'],
'日本區暨代工業務部': ['經副理', '課長', '專員', '助理'],
'歐亞區業務部': ['經副理', '課長', '專員', '助理'],
'韓國區業務部-韓國區': ['經副理', '課長', '專員', '助理', '專案經理'],
'美洲區業務部': ['經副理', '課長', '專員', '助理'],
'應用工程部(GTS)': ['經副理', '專案經副理', '技術經副理', '工程師'],
'系統工程部': ['經副理', '工程師'],
'特性測試部': ['經副理', '課長', '工程師'],
'業務生管部': ['經副理', '課長', '專員'],
'市場行銷企劃部': ['處長', '經理', '專員'],
'MOSFET晶圓採購部': ['經副理', '課長', '專員'],
'台灣區業務部': ['專員', '助理'],
'業務一部': ['處長/資深經理', '經副理', '專員', '助理'],
'業務二部': ['處長/資深經理', '經副理', '專員', '助理']
};
// ==================== API 資料載入函數 ====================
/**
* 從 API 載入組織階層資料,覆蓋預設資料
* @returns {Promise<boolean>} 是否載入成功
*/
export async function loadHierarchyData() {
try {
const response = await fetch('/api/hierarchy/cascade');
if (response.ok) {
const result = await response.json();
if (result.success && result.data) {
// 更新級聯映射資料
if (result.data.businessToDivision) {
Object.assign(businessToDivision, result.data.businessToDivision);
}
if (result.data.divisionToDepartment) {
Object.assign(divisionToDepartment, result.data.divisionToDepartment);
}
if (result.data.departmentToPosition) {
Object.assign(departmentToPosition, result.data.departmentToPosition);
}
console.log('[Hierarchy] 組織階層資料載入成功');
return true;
}
}
return false;
} catch (error) {
console.warn('[Hierarchy] 無法從 API 載入組織階層資料,使用預設資料:', error.message);
return false;
}
}
// ==================== 工具函數 ====================
/**
* 取得指定事業體的處級單位列表
* @param {string} businessUnit - 事業體名稱
* @returns {string[]} 處級單位列表
*/
export function getDivisions(businessUnit) {
return businessToDivision[businessUnit] || [];
}
/**
* 取得指定處級單位的部級單位列表
* @param {string} division - 處級單位名稱
* @returns {string[]} 部級單位列表
*/
export function getDepartments(division) {
return divisionToDepartment[division] || [];
}
/**
* 取得指定部級單位的崗位名稱列表
* @param {string} department - 部級單位名稱
* @returns {string[]} 崗位名稱列表
*/
export function getPositions(department) {
return departmentToPosition[department] || [];
}

333
js/dropdowns.js Normal file
View File

@@ -0,0 +1,333 @@
/**
* 下拉選單模組
* 處理階層式下拉選單的連動
*/
import {
businessUnits,
businessToDivision,
divisionToDepartment,
departmentToPosition,
loadHierarchyData
} from './data/hierarchy.js';
// ==================== 初始化下拉選單 ====================
export function initializeDropdowns() {
// 初始化崗位基礎資料維護的事業體下拉選單
const businessUnitSelect = document.getElementById('businessUnit');
if (businessUnitSelect) {
businessUnitSelect.innerHTML = '<option value="">請選擇</option>';
businessUnits.forEach(unit => {
const option = document.createElement('option');
option.value = unit;
option.textContent = unit;
businessUnitSelect.appendChild(option);
});
businessUnitSelect.addEventListener('change', onBusinessUnitChange);
}
// 初始化處級單位下拉選單
const divisionSelect = document.getElementById('division');
if (divisionSelect) {
divisionSelect.innerHTML = '<option value="">請先選擇事業體</option>';
divisionSelect.addEventListener('change', onDivisionChange);
}
// 初始化部級單位下拉選單
const departmentSelect = document.getElementById('department');
if (departmentSelect) {
departmentSelect.innerHTML = '<option value="">請先選擇處級單位</option>';
}
// ========== 初始化崗位描述模組的下拉選單 ==========
const jdBusinessUnitSelect = document.getElementById('jd_businessUnit');
if (jdBusinessUnitSelect) {
jdBusinessUnitSelect.innerHTML = '<option value="">請選擇</option>';
businessUnits.forEach(unit => {
const option = document.createElement('option');
option.value = unit;
option.textContent = unit;
jdBusinessUnitSelect.appendChild(option);
});
jdBusinessUnitSelect.addEventListener('change', onJobDescBusinessUnitChange);
}
// 崗位描述的處級單位
const jdDivisionSelect = document.getElementById('jd_division');
if (jdDivisionSelect) {
jdDivisionSelect.addEventListener('change', onJobDescDivisionChange);
}
// 崗位描述的部級單位
const jdDepartmentSelect = document.getElementById('jd_department');
if (jdDepartmentSelect) {
jdDepartmentSelect.addEventListener('change', onJobDescDepartmentChange);
}
// ========== 初始化部門職責維護模組的下拉選單 ==========
const deptFuncBusinessUnitSelect = document.getElementById('deptFunc_businessUnit');
if (deptFuncBusinessUnitSelect) {
deptFuncBusinessUnitSelect.innerHTML = '<option value="">請選擇</option>';
businessUnits.forEach(unit => {
const option = document.createElement('option');
option.value = unit;
option.textContent = unit;
deptFuncBusinessUnitSelect.appendChild(option);
});
deptFuncBusinessUnitSelect.addEventListener('change', onDeptFuncBusinessUnitChange);
}
// 部門職責的處級單位
const deptFuncDivisionSelect = document.getElementById('deptFunc_division');
if (deptFuncDivisionSelect) {
deptFuncDivisionSelect.addEventListener('change', onDeptFuncDivisionChange);
}
// 部門職責的部級單位
const deptFuncDepartmentSelect = document.getElementById('deptFunc_department');
if (deptFuncDepartmentSelect) {
deptFuncDepartmentSelect.addEventListener('change', onDeptFuncDepartmentChange);
}
}
// ==================== 崗位基礎資料維護的連動 ====================
function onBusinessUnitChange(event) {
const selectedBusiness = event.target.value;
const divisionSelect = document.getElementById('division');
const departmentSelect = document.getElementById('department');
if (divisionSelect) {
divisionSelect.innerHTML = '<option value="">請選擇</option>';
}
if (departmentSelect) {
departmentSelect.innerHTML = '<option value="">請先選擇處級單位</option>';
}
if (selectedBusiness && businessToDivision[selectedBusiness]) {
const divisions = businessToDivision[selectedBusiness];
divisions.forEach(division => {
const option = document.createElement('option');
option.value = division;
option.textContent = division;
divisionSelect.appendChild(option);
});
}
}
function onDivisionChange(event) {
const selectedDivision = event.target.value;
const departmentSelect = document.getElementById('department');
if (departmentSelect) {
departmentSelect.innerHTML = '<option value="">請選擇</option>';
}
if (selectedDivision && divisionToDepartment[selectedDivision]) {
const departments = divisionToDepartment[selectedDivision];
departments.forEach(department => {
const option = document.createElement('option');
option.value = department;
option.textContent = department;
departmentSelect.appendChild(option);
});
}
}
// ==================== 崗位描述模組的階層式下拉選單 ====================
function onJobDescBusinessUnitChange(event) {
const selectedBusiness = event.target.value;
const divisionSelect = document.getElementById('jd_division');
const departmentSelect = document.getElementById('jd_department');
const positionSelect = document.getElementById('jd_positionTitle');
if (divisionSelect) divisionSelect.innerHTML = '<option value="">請選擇</option>';
if (departmentSelect) departmentSelect.innerHTML = '<option value="">請先選擇處級單位</option>';
if (positionSelect) positionSelect.innerHTML = '<option value="">請先選擇部級單位</option>';
if (selectedBusiness && businessToDivision[selectedBusiness]) {
const divisions = businessToDivision[selectedBusiness];
divisions.forEach(division => {
const option = document.createElement('option');
option.value = division;
option.textContent = division;
divisionSelect.appendChild(option);
});
}
}
function onJobDescDivisionChange(event) {
const selectedDivision = event.target.value;
const departmentSelect = document.getElementById('jd_department');
const positionSelect = document.getElementById('jd_positionTitle');
if (departmentSelect) departmentSelect.innerHTML = '<option value="">請選擇</option>';
if (positionSelect) positionSelect.innerHTML = '<option value="">請先選擇部級單位</option>';
if (selectedDivision && divisionToDepartment[selectedDivision]) {
const departments = divisionToDepartment[selectedDivision];
departments.forEach(department => {
const option = document.createElement('option');
option.value = department;
option.textContent = department;
departmentSelect.appendChild(option);
});
}
}
function onJobDescDepartmentChange(event) {
const selectedDepartment = event.target.value;
const positionSelect = document.getElementById('jd_positionTitle');
if (positionSelect) positionSelect.innerHTML = '<option value="">請選擇</option>';
if (selectedDepartment && departmentToPosition[selectedDepartment]) {
const positions = departmentToPosition[selectedDepartment];
positions.forEach(position => {
const option = document.createElement('option');
option.value = position;
option.textContent = position;
positionSelect.appendChild(option);
});
}
}
// ==================== 部門職責維護模組的階層式下拉選單 ====================
function onDeptFuncBusinessUnitChange(event) {
const selectedBusiness = event.target.value;
const divisionSelect = document.getElementById('deptFunc_division');
const departmentSelect = document.getElementById('deptFunc_department');
const positionSelect = document.getElementById('deptFunc_positionTitle');
if (divisionSelect) divisionSelect.innerHTML = '<option value="">請選擇</option>';
if (departmentSelect) departmentSelect.innerHTML = '<option value="">請先選擇處級單位</option>';
if (positionSelect) positionSelect.innerHTML = '<option value="">請先選擇部級單位</option>';
if (selectedBusiness && businessToDivision[selectedBusiness]) {
const divisions = businessToDivision[selectedBusiness];
divisions.forEach(division => {
const option = document.createElement('option');
option.value = division;
option.textContent = division;
divisionSelect.appendChild(option);
});
}
}
function onDeptFuncDivisionChange(event) {
const selectedDivision = event.target.value;
const departmentSelect = document.getElementById('deptFunc_department');
const positionSelect = document.getElementById('deptFunc_positionTitle');
if (departmentSelect) departmentSelect.innerHTML = '<option value="">請選擇</option>';
if (positionSelect) positionSelect.innerHTML = '<option value="">請先選擇部級單位</option>';
if (selectedDivision && divisionToDepartment[selectedDivision]) {
const departments = divisionToDepartment[selectedDivision];
departments.forEach(department => {
const option = document.createElement('option');
option.value = department;
option.textContent = department;
departmentSelect.appendChild(option);
});
}
}
function onDeptFuncDepartmentChange(event) {
const selectedDepartment = event.target.value;
const positionSelect = document.getElementById('deptFunc_positionTitle');
if (positionSelect) positionSelect.innerHTML = '<option value="">請選擇</option>';
if (selectedDepartment && departmentToPosition[selectedDepartment]) {
const positions = departmentToPosition[selectedDepartment];
positions.forEach(position => {
const option = document.createElement('option');
option.value = position;
option.textContent = position;
positionSelect.appendChild(option);
});
}
}
// ==================== 部門職責關聯功能 ====================
// 部門職責資料 (示範用)
let deptFunctionData = [];
export function refreshDeptFunctionList(showMessage = false) {
const select = document.getElementById('jd_deptFunction');
if (!select) return;
select.innerHTML = '<option value="">-- 請選擇部門職責 --</option>';
if (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);
});
if (showMessage && typeof showToast === 'function') {
showToast('已載入 ' + deptFunctionData.length + ' 筆部門職責資料');
}
} else {
if (showMessage && typeof showToast === 'function') {
showToast('尚無部門職責資料,請先建立部門職責');
}
}
}
export function loadDeptFunctionInfo() {
const select = document.getElementById('jd_deptFunction');
const infoSection = document.getElementById('deptFunctionInfoSection');
if (!select) return;
const selectedCode = select.value;
if (!selectedCode) {
if (infoSection) infoSection.style.display = 'none';
return;
}
const deptFunc = deptFunctionData.find(d => d.deptFunctionCode === selectedCode);
if (deptFunc) {
const codeEl = document.getElementById('jd_deptFunctionCode');
const buEl = document.getElementById('jd_deptFunctionBU');
const missionEl = document.getElementById('jd_deptMission');
const functionsEl = document.getElementById('jd_deptCoreFunctions');
const kpisEl = document.getElementById('jd_deptKPIs');
if (codeEl) codeEl.value = deptFunc.deptFunctionCode || '';
if (buEl) buEl.value = deptFunc.deptFunctionBU || '';
if (missionEl) missionEl.value = deptFunc.deptMission || '';
if (functionsEl) functionsEl.value = deptFunc.deptCoreFunctions || '';
if (kpisEl) kpisEl.value = deptFunc.deptKPIs || '';
const deptInput = document.getElementById('jd_department');
if (deptInput && !deptInput.value) {
deptInput.value = deptFunc.deptFunctionDept;
}
if (infoSection) infoSection.style.display = 'block';
if (typeof showToast === 'function') {
showToast('已載入部門職責: ' + deptFunc.deptFunctionName);
}
}
}
export function setDeptFunctionData(data) {
deptFunctionData = data;
}
// 初始化時載入階層資料
export async function initializeHierarchyDropdowns() {
await loadHierarchyData();
initializeDropdowns();
}

571
js/forms.js Normal file
View File

@@ -0,0 +1,571 @@
/**
* 表單邏輯模組
* 處理各表單的資料操作、驗證和提交
*/
const API_BASE_URL = '/api';
// ==================== 常數映射 ====================
export const categoryMap = { '01': '技術職', '02': '管理職', '03': '業務職', '04': '行政職' };
export const natureMap = { 'FT': '全職', 'PT': '兼職', 'CT': '約聘', 'IN': '實習' };
export const jobCategoryMap = { 'MGR': '管理職', 'TECH': '技術職', 'SALE': '業務職', 'ADMIN': '行政職', 'RD': '研發職', 'PROD': '生產職' };
// ==================== 崗位清單全域變數 ====================
export let positionListData = [];
export let currentSortColumn = '';
export let currentSortDirection = 'asc';
// ==================== 崗位基礎資料表單 ====================
export function updateCategoryName() {
const category = document.getElementById('positionCategory').value;
document.getElementById('positionCategoryName').value = categoryMap[category] || '';
updatePreview();
}
export function updateNatureName() {
const nature = document.getElementById('positionNature').value;
document.getElementById('positionNatureName').value = natureMap[nature] || '';
updatePreview();
}
export function changePositionCode() {
const currentCode = document.getElementById('positionCode').value;
const newCode = prompt('請輸入新的崗位編號:', currentCode);
if (newCode && newCode !== currentCode) {
document.getElementById('positionCode').value = newCode;
showToast('崗位編號已更改!');
updatePreview();
}
}
export function getPositionFormData() {
const form = document.getElementById('positionForm');
const formData = new FormData(form);
const data = { basicInfo: {}, recruitInfo: {} };
const basicFields = ['positionCode', 'positionName', 'positionCategory', 'positionCategoryName', 'positionNature', 'positionNatureName', 'headcount', 'positionLevel', 'effectiveDate', 'positionDesc', 'positionRemark'];
const recruitFields = ['minEducation', 'requiredGender', 'salaryRange', 'workExperience', 'minAge', 'maxAge', 'jobType', 'recruitPosition', 'jobTitle', 'jobDesc', 'positionReq', 'titleReq', 'majorReq', 'skillReq', 'langReq', 'otherReq', 'superiorPosition', 'recruitRemark'];
basicFields.forEach(field => { const value = formData.get(field); if (value) data.basicInfo[field] = value; });
recruitFields.forEach(field => { const value = formData.get(field); if (value) data.recruitInfo[field] = value; });
return data;
}
export function savePositionAndExit() {
const form = document.getElementById('positionForm');
if (!form.checkValidity()) { form.reportValidity(); return; }
console.log('Save Position:', getPositionFormData());
showToast('崗位資料已保存!');
}
export function savePositionAndNew() {
const form = document.getElementById('positionForm');
if (!form.checkValidity()) { form.reportValidity(); return; }
console.log('Save Position:', getPositionFormData());
showToast('崗位資料已保存,請繼續新增!');
form.reset();
document.getElementById('effectiveDate').value = new Date().toISOString().split('T')[0];
updatePreview();
}
export async function saveToPositionList() {
const form = document.getElementById('positionForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = getPositionFormData();
if (!formData.basicInfo.positionCode) {
alert('請輸入崗位編號');
return;
}
if (!formData.basicInfo.positionName) {
alert('請輸入崗位名稱');
return;
}
try {
const response = await fetch(API_BASE_URL + '/positions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
showToast(result.message || '崗位已成功儲存至崗位清單!');
setTimeout(() => {
switchModule('positionlist');
}, 1500);
} else {
alert('儲存失敗: ' + (result.error || '未知錯誤'));
}
} catch (error) {
console.error('儲存錯誤:', error);
alert('儲存失敗: ' + error.message);
}
}
export function cancelPositionForm() {
if (confirm('確定要取消嗎?未保存的資料將會遺失。')) {
document.getElementById('positionForm').reset();
updatePreview();
}
}
// ==================== 職務基礎資料表單 ====================
export function updateJobCategoryName() {
const category = document.getElementById('jobCategoryCode').value;
document.getElementById('jobCategoryName').value = jobCategoryMap[category] || '';
updatePreview();
}
export function changeJobCode() {
const currentCode = document.getElementById('jobCode').value;
const newCode = prompt('請輸入新的職務編號:', currentCode);
if (newCode && newCode !== currentCode) {
document.getElementById('jobCode').value = newCode;
showToast('職務編號已更改!');
updatePreview();
}
}
export function getJobFormData() {
const form = document.getElementById('jobForm');
const formData = new FormData(form);
const data = {};
const fields = ['jobCategoryCode', 'jobCategoryName', 'jobCode', 'jobName', 'jobNameEn', 'jobEffectiveDate', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel'];
fields.forEach(field => { const value = formData.get(field); if (value) data[field] = value; });
data.hasAttendanceBonus = document.getElementById('job_hasAttBonus')?.checked || false;
data.hasHousingAllowance = document.getElementById('job_hasHouseAllow')?.checked || false;
return data;
}
export async function saveJobToPositionList() {
const form = document.getElementById('jobForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = getJobFormData();
if (!formData.jobCode) {
alert('請輸入職務代碼');
return;
}
if (!formData.jobName) {
alert('請輸入職務名稱');
return;
}
const positionData = {
basicInfo: {
positionCode: formData.jobCode,
positionName: formData.jobName,
positionCategory: formData.jobCategoryCode || '',
effectiveDate: formData.jobEffectiveDate || new Date().toISOString().split('T')[0],
headcount: formData.jobHeadcount || 1,
positionLevel: formData.jobLevel || '',
positionRemark: formData.jobRemark || ''
},
recruitInfo: {}
};
try {
const response = await fetch(API_BASE_URL + '/positions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(positionData)
});
const result = await response.json();
if (result.success) {
showToast(result.message || '職務已成功儲存至崗位清單!');
setTimeout(() => {
switchModule('positionlist');
}, 1500);
} else {
alert('儲存失敗: ' + (result.error || '未知錯誤'));
}
} catch (error) {
console.error('儲存錯誤:', error);
alert('儲存失敗: ' + error.message);
}
}
export function saveJobAndExit() {
const form = document.getElementById('jobForm');
if (!form.checkValidity()) { form.reportValidity(); return; }
console.log('Save Job:', getJobFormData());
showToast('職務資料已保存!');
}
export function saveJobAndNew() {
const form = document.getElementById('jobForm');
if (!form.checkValidity()) { form.reportValidity(); return; }
console.log('Save Job:', getJobFormData());
showToast('職務資料已保存,請繼續新增!');
form.reset();
document.getElementById('attendanceLabel').textContent = '否';
document.getElementById('housingLabel').textContent = '否';
updatePreview();
}
export function cancelJobForm() {
if (confirm('確定要取消嗎?未保存的資料將會遺失。')) {
document.getElementById('jobForm').reset();
document.getElementById('attendanceLabel').textContent = '否';
document.getElementById('housingLabel').textContent = '否';
updatePreview();
}
}
// ==================== 崗位描述表單 ====================
export function getJobDescFormData() {
const form = document.getElementById('jobDescForm');
const formData = new FormData(form);
const data = { basicInfo: {}, positionInfo: {}, responsibilities: {}, requirements: {} };
['empNo', 'empName', 'positionCode', 'versionDate'].forEach(field => {
const el = document.getElementById('jd_' + field);
if (el && el.value) data.basicInfo[field] = el.value;
});
['positionName', 'department', 'positionEffectiveDate', 'directSupervisor', 'positionGradeJob', 'reportTo', 'directReports', 'workLocation', 'empAttribute'].forEach(field => {
const el = document.getElementById('jd_' + field);
if (el && el.value) data.positionInfo[field] = el.value;
});
const purpose = document.getElementById('jd_positionPurpose');
if (purpose && purpose.value) data.responsibilities.positionPurpose = purpose.value;
const mainResp = document.getElementById('jd_mainResponsibilities');
if (mainResp && mainResp.value) data.responsibilities.mainResponsibilities = mainResp.value;
['education', 'basicSkills', 'professionalKnowledge', 'workExperienceReq', 'otherRequirements'].forEach(field => {
const el = document.getElementById('jd_' + field);
if (el && el.value) data.requirements[field] = el.value;
});
return data;
}
export async function saveJobDescAndExit() {
const formData = getJobDescFormData();
console.log('Save JobDesc:', formData);
if (!formData.basicInfo.positionCode) {
alert('請輸入崗位代碼');
return;
}
const requestData = {
positionCode: formData.basicInfo.positionCode,
positionName: formData.positionInfo.positionName || '',
effectiveDate: formData.positionInfo.positionEffectiveDate || new Date().toISOString().split('T')[0],
jobDuties: formData.responsibilities.mainResponsibilities || '',
requiredSkills: formData.requirements.basicSkills || '',
workEnvironment: formData.positionInfo.workLocation || ''
};
try {
const response = await fetch(API_BASE_URL + '/position-descriptions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
});
const result = await response.json();
if (result.success) {
showToast(result.message || '崗位描述已保存!');
setTimeout(() => {
switchModule('positionlist');
}, 1000);
} else {
alert('保存失敗: ' + (result.error || '未知錯誤'));
}
} catch (error) {
console.error('保存錯誤:', error);
alert('保存失敗: ' + error.message);
}
}
export async function saveJobDescAndNew() {
const formData = getJobDescFormData();
console.log('Save JobDesc:', formData);
if (!formData.basicInfo.positionCode) {
alert('請輸入崗位代碼');
return;
}
const requestData = {
positionCode: formData.basicInfo.positionCode,
positionName: formData.positionInfo.positionName || '',
effectiveDate: formData.positionInfo.positionEffectiveDate || new Date().toISOString().split('T')[0],
jobDuties: formData.responsibilities.mainResponsibilities || '',
requiredSkills: formData.requirements.basicSkills || '',
workEnvironment: formData.positionInfo.workLocation || ''
};
try {
const response = await fetch(API_BASE_URL + '/position-descriptions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
});
const result = await response.json();
if (result.success) {
showToast(result.message || '崗位描述已保存,請繼續新增!');
document.getElementById('jobDescForm').reset();
document.getElementById('jd_mainResponsibilities').value = '1、\n2、\n3、\n4、';
updatePreview();
} else {
alert('保存失敗: ' + (result.error || '未知錯誤'));
}
} catch (error) {
console.error('保存錯誤:', error);
alert('保存失敗: ' + error.message);
}
}
export async function saveJobDescToPositionList() {
const formData = getJobDescFormData();
if (!formData.basicInfo.positionCode) {
alert('請輸入崗位代碼');
return;
}
const positionData = {
basicInfo: {
positionCode: formData.basicInfo.positionCode,
positionName: formData.positionInfo.positionName || '',
effectiveDate: formData.positionInfo.positionEffectiveDate || new Date().toISOString().split('T')[0],
positionDesc: formData.responsibilities.mainResponsibilities || '',
positionRemark: formData.responsibilities.positionPurpose || ''
},
recruitInfo: {
minEducation: formData.requirements.education || '',
skillReq: formData.requirements.basicSkills || '',
workExperience: formData.requirements.workExperienceReq || '',
otherReq: formData.requirements.otherRequirements || ''
}
};
try {
const response = await fetch(API_BASE_URL + '/positions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(positionData)
});
const result = await response.json();
if (result.success) {
showToast(result.message || '崗位描述已成功儲存至崗位清單!');
setTimeout(() => {
switchModule('positionlist');
}, 1500);
} else {
alert('儲存失敗: ' + (result.error || '未知錯誤'));
}
} catch (error) {
console.error('儲存錯誤:', error);
alert('儲存失敗: ' + error.message);
}
}
export function cancelJobDescForm() {
if (confirm('確定要取消嗎?未保存的資料將會遺失。')) {
document.getElementById('jobDescForm').reset();
document.getElementById('jd_mainResponsibilities').value = '1、\n2、\n3、\n4、';
updatePreview();
}
}
// ==================== 預覽更新 ====================
export function updatePreview() {
const activeModuleBtn = document.querySelector('.module-btn.active');
if (!activeModuleBtn) return;
const activeModule = activeModuleBtn.dataset.module;
let data;
if (activeModule === 'position') {
data = { module: '崗位基礎資料', ...getPositionFormData() };
} else if (activeModule === 'job') {
data = { module: '職務基礎資料', ...getJobFormData() };
} else {
data = { module: '崗位描述', ...getJobDescFormData() };
}
const previewEl = document.getElementById('jsonPreview');
if (previewEl) {
previewEl.textContent = JSON.stringify(data, null, 2);
}
}
// ==================== Toast 訊息 ====================
export function showToast(message) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toastMessage');
if (!toast || !toastMessage) {
console.warn('Toast elements not found, creating dynamic toast');
const existingToast = document.querySelector('.toast.dynamic-toast');
if (existingToast) existingToast.remove();
const dynamicToast = document.createElement('div');
dynamicToast.className = 'toast dynamic-toast show';
dynamicToast.innerHTML = `
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
<span>${message}</span>
`;
document.body.appendChild(dynamicToast);
setTimeout(() => {
dynamicToast.classList.remove('show');
setTimeout(() => dynamicToast.remove(), 300);
}, 3000);
return;
}
toastMessage.textContent = message;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3000);
}
// ==================== 模組切換 ====================
export function switchModule(moduleName) {
document.querySelectorAll('.module-btn').forEach(b => {
b.classList.remove('active', 'job-active', 'desc-active');
});
document.querySelectorAll('.module-content').forEach(c => c.classList.remove('active'));
const targetBtn = document.querySelector(`.module-btn[data-module="${moduleName}"]`);
if (targetBtn) {
targetBtn.classList.add('active');
if (moduleName === 'job') targetBtn.classList.add('job-active');
if (moduleName === 'jobdesc') targetBtn.classList.add('desc-active');
}
const targetModule = document.getElementById('module-' + moduleName);
if (targetModule) {
targetModule.classList.add('active');
}
if (moduleName === 'positionlist') {
loadPositionList();
}
updatePreview();
}
// ==================== 崗位清單功能 ====================
export async function loadPositionList() {
try {
showToast('正在載入崗位清單...');
const response = await fetch(API_BASE_URL + '/position-list');
const result = await response.json();
if (result.success) {
positionListData = result.data;
renderPositionList();
showToast('已載入 ' + positionListData.length + ' 筆崗位資料');
} else {
alert('載入失敗: ' + (result.error || '未知錯誤'));
positionListData = [];
renderPositionList();
}
} catch (error) {
console.error('載入錯誤:', error);
alert('載入失敗: ' + error.message);
positionListData = [];
renderPositionList();
}
}
export function renderPositionList() {
const tbody = document.getElementById('positionListBody');
if (!tbody) return;
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;">${sanitizeHTML(item.positionCode)}</td>
<td style="padding: 12px;">${sanitizeHTML(item.positionName)}</td>
<td style="padding: 12px;">${sanitizeHTML(item.positionCategory || '')}</td>
<td style="padding: 12px;">${sanitizeHTML(item.positionNature || '')}</td>
<td style="padding: 12px;">${sanitizeHTML(String(item.headcount || ''))}</td>
<td style="padding: 12px;">${sanitizeHTML(item.positionLevel || '')}</td>
<td style="padding: 12px;">${sanitizeHTML(item.effectiveDate || '')}</td>
<td style="padding: 12px; text-align: center;">
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="viewPositionDesc('${sanitizeHTML(item.positionCode)}')">檢視</button>
</td>
</tr>
`).join('');
}
export 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 sanitizeHTML(str) {
if (str === null || str === undefined) return '';
const temp = document.createElement('div');
temp.textContent = str;
return temp.innerHTML;
}
// 暴露全域函數
if (typeof window !== 'undefined') {
window.showToast = showToast;
window.updatePreview = updatePreview;
window.switchModule = switchModule;
window.updateCategoryName = updateCategoryName;
window.updateNatureName = updateNatureName;
window.updateJobCategoryName = updateJobCategoryName;
}

206
js/main.js Normal file
View File

@@ -0,0 +1,206 @@
/**
* Main - 主程式
* 初始化應用程式,設定事件監聽器
*/
import { showToast } from './utils.js';
import { switchModule, updatePreview, updateCategoryName, updateNatureName, updateJobCategoryName } from './ui.js';
import { initializeBagTitles } from './ai-bags.js';
// ==================== 初始化 ====================
/**
* 載入用戶信息
*/
function loadUserInfo() {
const currentUser = localStorage.getItem('currentUser');
if (!currentUser) {
return;
}
try {
const userData = JSON.parse(currentUser);
const userName = document.getElementById('userName');
const userRole = document.getElementById('userRole');
const userAvatar = document.getElementById('userAvatar');
if (userName) userName.textContent = userData.name || '使用者';
if (userRole) userRole.textContent = userData.role || '一般使用者';
if (userAvatar) userAvatar.textContent = (userData.name || 'U').charAt(0).toUpperCase();
} catch (e) {
console.error('解析用戶資料失敗:', e);
}
}
/**
* 登出功能
*/
function logout() {
if (confirm('確定要登出嗎?')) {
localStorage.removeItem('currentUser');
window.location.href = 'login.html';
}
}
// ==================== 事件監聽器設置 ====================
/**
* 設置模組切換事件
*/
function setupModuleSwitching() {
document.querySelectorAll('.module-btn').forEach(btn => {
btn.addEventListener('click', () => {
const moduleName = btn.dataset.module;
if (moduleName) {
switchModule(moduleName);
}
});
});
}
/**
* 設置標籤頁切換事件
*/
function setupTabSwitching() {
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
const parent = btn.closest('.form-card');
if (!parent) return;
parent.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
parent.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
const targetTab = document.getElementById('tab-' + btn.dataset.tab);
if (targetTab) {
targetTab.classList.add('active');
}
});
});
}
/**
* 設置表單欄位監聽
*/
function setupFormListeners() {
// 監聽所有表單欄位變更,更新預覽
document.querySelectorAll('input, select, textarea').forEach(field => {
field.addEventListener('change', updatePreview);
field.addEventListener('input', updatePreview);
});
// 崗位類別變更 (pos_category)
const positionCategory = document.getElementById('pos_category');
if (positionCategory) {
positionCategory.addEventListener('change', updateCategoryName);
}
// 崗位性質變更 (pos_type)
const positionNature = document.getElementById('pos_type');
if (positionNature) {
positionNature.addEventListener('change', updateNatureName);
}
// 職務類別變更 (job_category)
const jobCategoryCode = document.getElementById('job_category');
if (jobCategoryCode) {
jobCategoryCode.addEventListener('change', updateJobCategoryName);
}
// Toggle 開關變更 (job_hasAttBonus)
const hasAttendanceBonus = document.getElementById('job_hasAttBonus');
if (hasAttendanceBonus) {
hasAttendanceBonus.addEventListener('change', function() {
const label = document.getElementById('attendanceLabel');
if (label) {
label.textContent = this.checked ? '是' : '否';
}
updatePreview();
});
}
// Toggle 開關變更 (job_hasHouseAllow)
const hasHousingAllowance = document.getElementById('job_hasHouseAllow');
if (hasHousingAllowance) {
hasHousingAllowance.addEventListener('change', function() {
const label = document.getElementById('housingLabel');
if (label) {
label.textContent = this.checked ? '是' : '否';
}
updatePreview();
});
}
}
/**
* 設置快捷鍵
*/
function setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Ctrl+S 或 Cmd+S: 保存當前模組
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
const activeModule = document.querySelector('.module-btn.active');
if (!activeModule) return;
const moduleName = activeModule.dataset.module;
if (moduleName === 'position' && typeof window.savePositionAndExit === 'function') {
window.savePositionAndExit();
} else if (moduleName === 'job' && typeof window.saveJobAndExit === 'function') {
window.saveJobAndExit();
} else if (moduleName === 'jobdesc' && typeof window.saveJobDescAndExit === 'function') {
window.saveJobDescAndExit();
} else if (moduleName === 'deptfunction' && typeof window.saveDeptFunctionAndExit === 'function') {
window.saveDeptFunctionAndExit();
}
}
// Ctrl+N 或 Cmd+N: 保存並新增下一筆
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
e.preventDefault();
const activeModule = document.querySelector('.module-btn.active');
if (!activeModule) return;
const moduleName = activeModule.dataset.module;
if (moduleName === 'position' && typeof window.savePositionAndNew === 'function') {
window.savePositionAndNew();
} else if (moduleName === 'job' && typeof window.saveJobAndNew === 'function') {
window.saveJobAndNew();
} else if (moduleName === 'jobdesc' && typeof window.saveJobDescAndNew === 'function') {
window.saveJobDescAndNew();
} else if (moduleName === 'deptfunction' && typeof window.saveDeptFunctionAndNew === 'function') {
window.saveDeptFunctionAndNew();
}
}
});
}
// ==================== DOMContentLoaded 初始化 ====================
document.addEventListener('DOMContentLoaded', () => {
console.log('🚀 HR 系統初始化中...');
// 載入用戶信息
loadUserInfo();
// 設置事件監聽器
setupModuleSwitching();
setupTabSwitching();
setupFormListeners();
setupKeyboardShortcuts();
// 初始化 AI 錦囊標題
initializeBagTitles();
// 初始化預覽
updatePreview();
console.log('✅ HR 系統初始化完成');
});
// ==================== 將函式掛載到 window ====================
if (typeof window !== 'undefined') {
window.logout = logout;
window.loadUserInfo = loadUserInfo;
}

88
js/prompts.js Normal file
View File

@@ -0,0 +1,88 @@
/**
* AI Prompt 模板模組
* 定義各表單的 AI 生成提示詞
*/
// ==================== AI 幫我想 - 預設 Prompt 模板 ====================
export const DEFAULT_AI_PROMPTS = {
positionBasic: `你是專業人資顧問,熟悉半導體製造業。請生成崗位基礎資料。
請生成以下欄位positionCode, positionName, positionCategory, positionNature, headcount, positionLevel, positionDesc, positionRemark
欄位說明:
- positionCode: 崗位編號(格式如 ENG-001, MGR-002
- positionName: 崗位名稱
- positionCategory: 崗位類別代碼01=技術職, 02=管理職, 03=業務職, 04=行政職)
- positionNature: 崗位性質代碼FT=全職, PT=兼職, CT=約聘, IN=實習)
- headcount: 編制人數1-10
- positionLevel: 崗位級別L1-L7
- positionDesc: 崗位描述(條列式)
- positionRemark: 崗位備注
請用繁體中文,只返回純 JSON 格式,不要有任何其他文字。`,
positionRecruit: `你是專業人資顧問,熟悉半導體製造業。請生成招聘要求資料。
請生成以下欄位minEducation, salaryRange, workExperience, jobType, recruitPosition, jobTitle, positionReq, skillReq, langReq
欄位說明:
- minEducation: 最低學歷代碼HS=高中職, JC=專科, BA=大學, MA=碩士)
- salaryRange: 薪酬范圍代碼A=30000以下, B=30000-50000, C=50000-80000, D=80000以上, N=面議)
- workExperience: 工作經驗年數0, 1, 3, 5, 10
- jobType: 工作性質代碼FT=全職, PT=兼職, CT=約聘)
- recruitPosition: 招聘職位代碼ENG=工程師, MGR=經理, AST=助理)
- jobTitle: 職位名稱
- positionReq: 崗位要求(條列式)
- skillReq: 技能要求(條列式)
- langReq: 語言要求(條列式)
請用繁體中文,只返回純 JSON 格式,不要有任何其他文字。`,
jobBasic: `你是專業人資顧問,熟悉半導體製造業。請生成職務基礎資料。
請生成以下欄位jobCode, jobName, jobNameEn, jobCategoryCode, jobLevel, jobHeadcount, jobRemark
欄位說明:
- jobCode: 職務編號(格式如 J001
- jobName: 職務名稱(繁體中文)
- jobNameEn: 職務名稱英文
- jobCategoryCode: 職務類別代碼MGR=管理職, TECH=技術職, ADMIN=行政職, SALE=業務職)
- jobLevel: 職務級別J1-J7
- jobHeadcount: 職務人數1-100
- jobRemark: 職務備註
請用繁體中文,只返回純 JSON 格式,不要有任何其他文字。`,
deptFunction: `你是專業人資顧問,熟悉半導體製造業。請生成部門職責資料。
請生成以下欄位deptFunctionCode, deptFunctionName, deptFunctionBU, deptFunctionDept, deptManager, deptMission, deptVision, deptCoreFunctions, deptKPIs
欄位說明:
- deptFunctionCode: 部門職責編號(格式如 DF001
- deptFunctionName: 部門職責名稱
- deptFunctionBU: 事業單位
- deptFunctionDept: 部門名稱
- deptManager: 部門主管
- deptMission: 部門使命
- deptVision: 部門願景
- deptCoreFunctions: 核心職能(條列式)
- deptKPIs: KPI 指標(條列式)
請用繁體中文,只返回純 JSON 格式,不要有任何其他文字。`,
jobDesc: `你是專業人資顧問,熟悉半導體製造業。請生成崗位描述資料。
請生成以下欄位positionName, department, directSupervisor, positionPurpose, mainResponsibilities, education, basicSkills, professionalKnowledge
欄位說明:
- positionName: 崗位名稱
- department: 所屬部門
- directSupervisor: 直屬主管
- positionPurpose: 崗位目的
- mainResponsibilities: 主要職責(條列式)
- education: 學歷要求
- basicSkills: 基本技能(條列式)
- professionalKnowledge: 專業知識(條列式)
請用繁體中文,只返回純 JSON 格式,不要有任何其他文字。`
};

391
js/ui.js Normal file
View File

@@ -0,0 +1,391 @@
/**
* UI - UI 操作函式
* 包含模組切換、預覽更新、表單資料收集
*/
import { showToast } from './utils.js';
import { categoryMap, natureMap, jobCategoryMap } from './config.js';
// ==================== 模組切換 ====================
/**
* 切換頁面模組
* @param {string} moduleName - 模組名稱position/job/jobdesc/positionlist/deptfunction/admin
*/
export function switchModule(moduleName) {
document.querySelectorAll('.module-btn').forEach(b => {
b.classList.remove('active', 'job-active', 'desc-active');
});
document.querySelectorAll('.module-content').forEach(c => c.classList.remove('active'));
const targetBtn = document.querySelector(`.module-btn[data-module="${moduleName}"]`);
if (targetBtn) {
targetBtn.classList.add('active');
if (moduleName === 'job') targetBtn.classList.add('job-active');
if (moduleName === 'jobdesc') targetBtn.classList.add('desc-active');
}
const targetModule = document.getElementById('module-' + moduleName);
if (targetModule) {
targetModule.classList.add('active');
}
// 自動刷新崗位清單(需要從其他模組匯入)
if (moduleName === 'positionlist' && typeof window.loadPositionList === 'function') {
window.loadPositionList();
}
updatePreview();
}
// ==================== 表單資料收集 ====================
/**
* 收集崗位表單資料
* @returns {Object} - 崗位資料(分為 basicInfo 和 recruitInfo
*/
export function getPositionFormData() {
const form = document.getElementById('positionForm');
const formData = new FormData(form);
const data = { basicInfo: {}, recruitInfo: {} };
// 使用新的 pos_ prefix 欄位
const basicFieldMapping = {
'pos_code': 'positionCode',
'pos_name': 'positionName',
'pos_category': 'positionCategory',
'pos_categoryName': 'positionCategoryName',
'pos_type': 'positionType',
'pos_typeName': 'positionTypeName',
'pos_headcount': 'headcount',
'pos_level': 'positionLevel',
'pos_effectiveDate': 'effectiveDate',
'pos_desc': 'positionDesc',
'pos_remark': 'positionRemark'
};
// 使用新的 rec_ prefix 欄位
const recruitFieldMapping = {
'rec_eduLevel': 'minEducation',
'rec_gender': 'requiredGender',
'rec_salaryRange': 'salaryRange',
'rec_expYears': 'workExperience',
'rec_minAge': 'minAge',
'rec_maxAge': 'maxAge',
'rec_jobType': 'jobType',
'rec_position': 'recruitPosition',
'rec_jobTitle': 'jobTitle',
'rec_jobDesc': 'jobDesc',
'rec_positionReq': 'positionReq',
'rec_certReq': 'certReq',
'rec_majorReq': 'majorReq',
'rec_skillReq': 'skillReq',
'rec_langReq': 'langReq',
'rec_otherReq': 'otherReq',
'rec_superiorCode': 'superiorPosition',
'rec_remark': 'recruitRemark'
};
Object.entries(basicFieldMapping).forEach(([htmlId, dataKey]) => {
const el = document.getElementById(htmlId);
if (el && el.value) data.basicInfo[dataKey] = el.value;
});
Object.entries(recruitFieldMapping).forEach(([htmlId, dataKey]) => {
const el = document.getElementById(htmlId);
if (el && el.value) data.recruitInfo[dataKey] = el.value;
});
return data;
}
/**
* 收集職務表單資料
* @returns {Object} - 職務資料
*/
export function getJobFormData() {
const form = document.getElementById('jobForm');
const data = {};
// 使用新的 job_ prefix 欄位
const fieldMapping = {
'job_category': 'jobCategoryCode',
'job_categoryName': 'jobCategoryName',
'job_code': 'jobCode',
'job_name': 'jobName',
'job_nameEn': 'jobNameEn',
'job_level': 'jobLevel',
'job_effectiveDate': 'jobEffectiveDate',
'job_sortOrder': 'jobSortOrder',
'job_headcount': 'jobHeadcount',
'job_remark': 'jobRemark'
};
Object.entries(fieldMapping).forEach(([htmlId, dataKey]) => {
const el = document.getElementById(htmlId);
if (el && el.value) data[dataKey] = el.value;
});
const hasAttBonus = document.getElementById('job_hasAttBonus');
const hasHouseAllow = document.getElementById('job_hasHouseAllow');
data.hasAttendanceBonus = hasAttBonus ? hasAttBonus.checked : false;
data.hasHousingAllowance = hasHouseAllow ? hasHouseAllow.checked : false;
return data;
}
/**
* 收集崗位描述表單資料
* @returns {Object} - 崗位描述資料
*/
export function getJobDescFormData() {
const form = document.getElementById('jobDescForm');
if (!form) return {};
const data = { basicInfo: {}, positionInfo: {}, responsibilities: {}, requirements: {} };
// Basic Info - 使用新的 jd_ prefix
const basicMapping = {
'jd_empNo': 'empNo',
'jd_empName': 'empName',
'jd_posCode': 'positionCode',
'jd_versionDate': 'versionDate'
};
Object.entries(basicMapping).forEach(([htmlId, dataKey]) => {
const el = document.getElementById(htmlId);
if (el && el.value) data.basicInfo[dataKey] = el.value;
});
// Position Info - 使用新的 jd_ prefix
const posInfoMapping = {
'jd_posName': 'positionName',
'jd_department': 'department',
'jd_posLevel': 'positionLevel',
'jd_posEffDate': 'positionEffectiveDate',
'jd_supervisor': 'directSupervisor',
'jd_gradeJob': 'positionGradeJob',
'jd_reportTo': 'reportTo',
'jd_directReports': 'directReports',
'jd_location': 'workLocation',
'jd_empAttr': 'empAttribute'
};
Object.entries(posInfoMapping).forEach(([htmlId, dataKey]) => {
const el = document.getElementById(htmlId);
if (el && el.value) data.positionInfo[dataKey] = el.value;
});
// Purpose & Responsibilities - 使用新的 jd_ prefix
const purpose = document.getElementById('jd_purpose');
if (purpose && purpose.value) data.responsibilities.positionPurpose = purpose.value;
const mainResp = document.getElementById('jd_mainResp');
if (mainResp && mainResp.value) data.responsibilities.mainResponsibilities = mainResp.value;
// Requirements - 使用新的 jd_ prefix
const reqMapping = {
'jd_eduLevel': 'education',
'jd_basicSkills': 'basicSkills',
'jd_proKnowledge': 'professionalKnowledge',
'jd_expReq': 'workExperienceReq',
'jd_otherReq': 'otherRequirements'
};
Object.entries(reqMapping).forEach(([htmlId, dataKey]) => {
const el = document.getElementById(htmlId);
if (el && el.value) data.requirements[dataKey] = el.value;
});
return data;
}
/**
* 收集部門職責表單資料
* @returns {Object} - 部門職責資料
*/
export function getDeptFunctionFormData() {
const form = document.getElementById('deptFunctionForm');
if (!form) return {};
const data = {};
// 使用新的 df_ prefix 欄位
const fieldMapping = {
'df_code': 'dfCode',
'df_name': 'dfName',
'df_businessUnit': 'businessUnit',
'df_division': 'division',
'df_department': 'department',
'df_section': 'section',
'df_posTitle': 'positionTitle',
'df_posLevel': 'positionLevel',
'df_managerTitle': 'managerTitle',
'df_effectiveDate': 'effectiveDate',
'df_headcountLimit': 'headcountLimit',
'df_status': 'status',
'df_mission': 'mission',
'df_vision': 'vision',
'df_coreFunc': 'coreFunctions',
'df_kpis': 'kpis',
'df_collab': 'collaboration',
'df_remark': 'remark'
};
Object.entries(fieldMapping).forEach(([htmlId, dataKey]) => {
const el = document.getElementById(htmlId);
if (el && el.value) data[dataKey] = el.value;
});
return data;
}
// ==================== 預覽更新 ====================
/**
* 更新 JSON 預覽
*/
export function updatePreview() {
const activeBtn = document.querySelector('.module-btn.active');
if (!activeBtn) return;
const activeModule = activeBtn.dataset.module;
let data;
if (activeModule === 'position') {
data = { module: '崗位基礎資料', ...getPositionFormData() };
} else if (activeModule === 'job') {
data = { module: '職務基礎資料', ...getJobFormData() };
} else if (activeModule === 'jobdesc') {
data = { module: '崗位描述', ...getJobDescFormData() };
} else if (activeModule === 'deptfunction') {
data = { module: '部門職責', ...getDeptFunctionFormData() };
} else {
return; // 其他模組不顯示預覽
}
const previewEl = document.getElementById('jsonPreview');
if (previewEl) {
previewEl.textContent = JSON.stringify(data, null, 2);
}
}
// ==================== 表單邏輯輔助函式 ====================
/**
* 更新崗位類別中文名稱
*/
export function updateCategoryName() {
const categoryEl = document.getElementById('pos_category');
const categoryNameEl = document.getElementById('pos_categoryName');
if (categoryEl && categoryNameEl) {
categoryNameEl.value = categoryMap[categoryEl.value] || '';
}
updatePreview();
}
/**
* 更新崗位性質中文名稱
*/
export function updateNatureName() {
const typeEl = document.getElementById('pos_type');
const typeNameEl = document.getElementById('pos_typeName');
if (typeEl && typeNameEl) {
typeNameEl.value = natureMap[typeEl.value] || '';
}
updatePreview();
}
/**
* 更新職務類別中文名稱
*/
export function updateJobCategoryName() {
const categoryEl = document.getElementById('job_category');
const categoryNameEl = document.getElementById('job_categoryName');
if (categoryEl && categoryNameEl) {
categoryNameEl.value = jobCategoryMap[categoryEl.value] || '';
}
updatePreview();
}
/**
* 修改崗位編號
*/
export function changePositionCode() {
const codeEl = document.getElementById('pos_code');
if (!codeEl) return;
const currentCode = codeEl.value;
const newCode = prompt('請輸入新的崗位編號:', currentCode);
if (newCode && newCode !== currentCode) {
codeEl.value = newCode;
showToast('崗位編號已更改!');
updatePreview();
}
}
/**
* 修改職務編號
*/
export function changeJobCode() {
const codeEl = document.getElementById('job_code');
if (!codeEl) return;
const currentCode = codeEl.value;
const newCode = prompt('請輸入新的職務編號:', currentCode);
if (newCode && newCode !== currentCode) {
codeEl.value = newCode;
showToast('職務編號已更改!');
updatePreview();
}
}
// ==================== 模態框函式(待整合)====================
/**
* 開啟專業科目選擇模態框
*/
export function openMajorModal() {
const modal = document.getElementById('majorModal');
if (modal) {
modal.classList.add('show');
}
}
/**
* 關閉專業科目選擇模態框
*/
export function closeMajorModal() {
const modal = document.getElementById('majorModal');
if (modal) {
modal.classList.remove('show');
}
}
/**
* 確認選擇專業科目
*/
export function confirmMajor() {
const selected = [];
document.querySelectorAll('#majorModal input[type="checkbox"]:checked').forEach(cb => {
selected.push(cb.value);
});
const majorReqEl = document.getElementById('rec_majorReq');
if (majorReqEl) {
majorReqEl.value = selected.join(', ');
}
closeMajorModal();
updatePreview();
}
// 將函式掛載到 window 上以便內聯事件處理器使用
if (typeof window !== 'undefined') {
window.switchModule = switchModule;
window.updateCategoryName = updateCategoryName;
window.updateNatureName = updateNatureName;
window.updateJobCategoryName = updateJobCategoryName;
window.changePositionCode = changePositionCode;
window.changeJobCode = changeJobCode;
window.openMajorModal = openMajorModal;
window.closeMajorModal = closeMajorModal;
window.confirmMajor = confirmMajor;
window.updatePreview = updatePreview;
}

206
js/utils.js Normal file
View File

@@ -0,0 +1,206 @@
/**
* Utilities - 工具函式
* 包含 XSS 防護、表單欄位工具、UI 回饋工具
*/
import { TOAST_DURATION } from './config.js';
// ==================== XSS 防護工具 ====================
/**
* 消毒 HTML 字串,防止 XSS 攻擊
* @param {string} str - 需要消毒的字串
* @returns {string} - 安全的字串
*/
export function sanitizeHTML(str) {
if (str === null || str === undefined) return '';
const temp = document.createElement('div');
temp.textContent = str;
return temp.innerHTML;
}
/**
* 安全設定元素文字內容
* @param {HTMLElement} element - 目標元素
* @param {string} text - 文字內容
*/
export function safeSetText(element, text) {
if (element) {
element.textContent = text;
}
}
// ==================== 表單欄位工具 ====================
/**
* 只在欄位為空時填入值
* @param {string} elementId - DOM 元素 ID
* @param {string} value - 要填入的值
* @returns {boolean} - 是否有填入值
*/
export function fillIfEmpty(elementId, value) {
const el = document.getElementById(elementId);
if (el && !el.value.trim() && value) {
el.value = value;
return true;
}
return false;
}
/**
* 獲取欄位當前值
* @param {string} elementId - DOM 元素 ID
* @returns {string} - 欄位值(已 trim
*/
export function getFieldValue(elementId) {
const el = document.getElementById(elementId);
return el ? el.value.trim() : '';
}
/**
* 獲取空白欄位列表
* @param {string[]} fieldIds - 欄位 ID 陣列
* @returns {string[]} - 空白欄位 ID 陣列
*/
export function getEmptyFields(fieldIds) {
return fieldIds.filter(id => !getFieldValue(id));
}
// ==================== UI 回饋工具 ====================
/**
* 顯示 Toast 提示訊息
* @param {string} message - 訊息內容
* @param {number} duration - 顯示時長(毫秒),預設 3000
*/
export function showToast(message, duration = TOAST_DURATION) {
const existingToast = document.querySelector('.toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = 'toast';
toast.innerHTML = `
<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>${sanitizeHTML(message)}</span>
`;
document.body.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, duration);
}
/**
* 設定按鈕載入狀態
* @param {HTMLElement} btn - 按鈕元素
* @param {boolean} loading - 是否載入中
*/
export function setButtonLoading(btn, loading) {
if (loading) {
btn.disabled = true;
btn.innerHTML = '<div class="spinner"></div><span>AI 生成中...</span>';
} else {
btn.disabled = false;
btn.innerHTML = '<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>';
}
}
// ==================== 錯誤處理工具 ====================
/**
* 顯示可複製的錯誤對話框
* @param {Object} options - 錯誤選項
* @param {string} options.title - 錯誤標題
* @param {string} options.message - 錯誤訊息
* @param {string} options.details - 錯誤詳情
* @param {string[]} options.suggestions - 建議列表
*/
export function showCopyableError(options) {
const { title, message, details, suggestions } = options;
// 移除舊的錯誤對話框
const existingModal = document.getElementById('errorModal');
if (existingModal) {
existingModal.remove();
}
const modalHTML = `
<div id="errorModal" class="modal-overlay show">
<div class="modal" style="max-width: min(90vw, 500px); max-height: 85vh; display: flex; flex-direction: column;">
<div class="modal-header" style="background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white; flex-shrink: 0;">
<h3 style="font-size: 1rem;">🚨 ${sanitizeHTML(title)}</h3>
<button class="modal-close" onclick="closeErrorModal(this)" style="color: white;">✕</button>
</div>
<div class="modal-body" style="overflow-y: auto; flex: 1; padding: 16px;">
<div style="margin-bottom: 12px;">
<strong style="font-size: 0.9rem;">錯誤訊息:</strong>
<p style="color: #e74c3c; font-weight: 500; font-size: 0.85rem; word-break: break-word;">${sanitizeHTML(message)}</p>
</div>
${details ? `
<div style="margin-bottom: 12px;">
<strong style="font-size: 0.9rem;">詳細資訊:</strong>
<pre style="background: #f8f9fa; padding: 10px; border-radius: 6px; overflow-x: auto; font-size: 0.75rem; max-height: 120px; white-space: pre-wrap; word-break: break-all;">${sanitizeHTML(details)}</pre>
</div>
` : ''}
${suggestions && suggestions.length > 0 ? `
<div>
<strong style="font-size: 0.9rem;">請檢查以下項目:</strong>
<ul style="margin: 6px 0; padding-left: 18px; font-size: 0.85rem;">
${suggestions.map(s => `<li style="margin: 3px 0;">${sanitizeHTML(s)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
<div class="modal-footer" style="flex-shrink: 0; padding: 12px 16px;">
<button class="btn btn-secondary" onclick="copyErrorDetails()" style="font-size: 0.85rem; padding: 8px 12px;">
<svg viewBox="0 0 24 24" style="width: 14px; height: 14px;">
<path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
複製
</button>
<button class="btn btn-primary" onclick="closeErrorModal(this)" style="font-size: 0.85rem; padding: 8px 16px;">關閉</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
/**
* 關閉錯誤對話框
* @param {HTMLElement} button - 按鈕元素
*/
export function closeErrorModal(button) {
const modal = button.closest('.modal-overlay');
if (modal) {
modal.remove();
}
}
/**
* 複製錯誤詳情到剪貼板
*/
export function copyErrorDetails() {
const modal = document.getElementById('errorModal');
if (!modal) return;
const errorText = modal.querySelector('.modal-body').textContent;
navigator.clipboard.writeText(errorText).then(() => {
showToast('錯誤詳情已複製到剪貼板');
}).catch(err => {
console.error('複製失敗:', err);
});
}
// 將函式掛載到 window 上以便內聯事件處理器使用
if (typeof window !== 'undefined') {
window.closeErrorModal = closeErrorModal;
window.copyErrorDetails = copyErrorDetails;
}

View File

@@ -7,6 +7,10 @@ import os
import requests import requests
from dotenv import load_dotenv from dotenv import load_dotenv
from typing import Dict, List, Tuple from typing import Dict, List, Tuple
import urllib3
# Disable SSL warnings for Ollama endpoint
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Load environment variables # Load environment variables
load_dotenv() load_dotenv()
@@ -34,6 +38,20 @@ class LLMConfig:
'api_key': os.getenv('OPENAI_API_KEY', ''), 'api_key': os.getenv('OPENAI_API_KEY', ''),
'endpoint': os.getenv('OPENAI_API_URL', 'https://api.openai.com/v1'), 'endpoint': os.getenv('OPENAI_API_URL', 'https://api.openai.com/v1'),
'enabled': bool(os.getenv('OPENAI_API_KEY')) 'enabled': bool(os.getenv('OPENAI_API_KEY'))
},
'ollama': {
'name': 'Ollama',
'api_key': '', # Ollama 不需要 API Key
'endpoint': os.getenv('OLLAMA_API_URL', 'https://ollama_pjapi.theaken.com'),
'model': os.getenv('OLLAMA_MODEL', 'qwen2.5:3b'),
'enabled': True # Ollama 預設啟用
},
'gptoss': {
'name': 'GPT-OSS',
'api_key': '', # GPT-OSS 不需要 API Key (使用 Ollama 介面)
'endpoint': os.getenv('GPTOSS_API_URL', 'https://ollama_pjapi.theaken.com'),
'model': os.getenv('GPTOSS_MODEL', 'gpt-oss:120b'),
'enabled': True # GPT-OSS 預設啟用
} }
} }
@@ -153,6 +171,35 @@ class LLMConfig:
except Exception as e: except Exception as e:
return False, f"錯誤: {str(e)}" return False, f"錯誤: {str(e)}"
def test_ollama_connection(self) -> Tuple[bool, str]:
"""Test Ollama API connection"""
try:
endpoint = self.apis['ollama']['endpoint']
# Test endpoint - list models
url = f"{endpoint}/v1/models"
response = requests.get(url, timeout=10, verify=False)
if response.status_code == 200:
data = response.json()
models = data.get('data', [])
if models:
model_count = len(models)
model_names = [m.get('id', '') for m in models[:3]]
return True, f"連線成功!找到 {model_count} 個可用模型 (例如: {', '.join(model_names)})"
else:
return True, "連線成功!"
else:
return False, f"連線失敗 (HTTP {response.status_code})"
except requests.exceptions.Timeout:
return False, "連線逾時"
except requests.exceptions.ConnectionError:
return False, "無法連接到伺服器"
except Exception as e:
return False, f"錯誤: {str(e)}"
def test_all_connections(self) -> Dict[str, Tuple[bool, str]]: def test_all_connections(self) -> Dict[str, Tuple[bool, str]]:
"""Test all configured API connections""" """Test all configured API connections"""
results = {} results = {}
@@ -166,6 +213,9 @@ class LLMConfig:
if self.apis['openai']['enabled']: if self.apis['openai']['enabled']:
results['openai'] = self.test_openai_connection() results['openai'] = self.test_openai_connection()
if self.apis['ollama']['enabled']:
results['ollama'] = self.test_ollama_connection()
return results return results
def generate_text_gemini(self, prompt: str, max_tokens: int = 2000) -> Tuple[bool, str]: def generate_text_gemini(self, prompt: str, max_tokens: int = 2000) -> Tuple[bool, str]:
@@ -175,7 +225,9 @@ class LLMConfig:
if not api_key: if not api_key:
return False, "API Key 未設定" return False, "API Key 未設定"
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}" # 從環境變數讀取模型名稱,默認使用 gemini-1.5-flash
model_name = os.getenv('GEMINI_MODEL', 'gemini-1.5-flash')
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent?key={api_key}"
data = { data = {
"contents": [ "contents": [
@@ -275,6 +327,96 @@ class LLMConfig:
except Exception as e: except Exception as e:
return False, f"錯誤: {str(e)}" return False, f"錯誤: {str(e)}"
def generate_text_ollama(self, prompt: str, max_tokens: int = 2000, model: str = None) -> Tuple[bool, str]:
"""Generate text using Ollama API
Args:
prompt: The prompt text
max_tokens: Maximum tokens to generate (not used by Ollama but kept for compatibility)
model: The model to use. If None, uses the default from config.
"""
try:
endpoint = self.apis['ollama']['endpoint']
# 使用傳入的 model 參數,如果沒有則使用設定檔中的預設值
if model is None:
model = self.apis['ollama']['model']
url = f"{endpoint}/v1/chat/completions"
headers = {
'Content-Type': 'application/json'
}
data = {
"model": model,
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt}
],
"temperature": 0.7
}
# 增加逾時時間到 180 秒,處理大模型較慢的情況
response = requests.post(url, json=data, headers=headers, timeout=180, verify=False)
if response.status_code == 200:
result = response.json()
text = result['choices'][0]['message']['content']
return True, text
else:
return False, f"生成失敗 (HTTP {response.status_code}): {response.text}"
except requests.exceptions.Timeout:
return False, f"連線逾時 (180秒)伺服器可能過載或網路不穩定。建議1) 稍後重試 2) 使用本地 Ollama (localhost:11434) 3) 切換到 Gemini API"
except requests.exceptions.ConnectionError:
return False, "無法連接到 Ollama 伺服器,請確認伺服器地址正確且服務已啟動"
except Exception as e:
return False, f"錯誤: {str(e)}"
def generate_text_gptoss(self, prompt: str, max_tokens: int = 2000, model: str = None) -> Tuple[bool, str]:
"""Generate text using GPT-OSS API (120B model via Ollama interface)
Args:
prompt: The prompt text
max_tokens: Maximum tokens to generate (not used by Ollama but kept for compatibility)
model: The model to use. If None, uses the default from config.
"""
try:
endpoint = self.apis['gptoss']['endpoint']
# 使用傳入的 model 參數,如果沒有則使用設定檔中的預設值
if model is None:
model = self.apis['gptoss']['model']
url = f"{endpoint}/v1/chat/completions"
headers = {
'Content-Type': 'application/json'
}
data = {
"model": model,
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt}
],
"temperature": 0.7
}
# 增加逾時時間到 180 秒
response = requests.post(url, json=data, headers=headers, timeout=180, verify=False)
if response.status_code == 200:
result = response.json()
text = result['choices'][0]['message']['content']
return True, text
else:
return False, f"生成失敗 (HTTP {response.status_code}): {response.text}"
except requests.exceptions.Timeout:
return False, f"連線逾時 (180秒)伺服器可能過載或網路不穩定。建議1) 稍後重試 2) 使用本地 Ollama (localhost:11434) 3) 切換到 Gemini API"
except requests.exceptions.ConnectionError:
return False, "無法連接到 GPT-OSS 伺服器,請確認伺服器地址正確且服務已啟動"
except Exception as e:
return False, f"錯誤: {str(e)}"
def main(): def main():
"""Test script""" """Test script"""

471
login.html Normal file
View File

@@ -0,0 +1,471 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>那都AI寫的不要問我 - 登入</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft JhengHei', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.login-container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
max-width: 450px;
width: 100%;
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px 30px;
text-align: center;
color: white;
}
.logo-container {
margin-bottom: 20px;
}
.logo-container img {
width: 150px;
height: 150px;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
.system-title {
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
.system-subtitle {
font-size: 14px;
opacity: 0.9;
font-style: italic;
}
.login-body {
padding: 40px 30px;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 15px;
transition: all 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
width: 100%;
padding: 14px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.btn-primary:active {
transform: translateY(0);
}
.divider {
text-align: center;
margin: 30px 0 20px 0;
position: relative;
}
.divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #e0e0e0;
}
.divider span {
background: white;
padding: 0 15px;
color: #999;
font-size: 14px;
position: relative;
z-index: 1;
}
.quick-login {
margin-top: 20px;
}
.quick-login-title {
font-size: 14px;
color: #666;
margin-bottom: 15px;
text-align: center;
font-weight: 500;
}
.btn-test {
background: white;
color: #333;
border: 2px solid #e0e0e0;
font-size: 14px;
padding: 12px;
}
.btn-test:hover {
background: #f8f9fa;
border-color: #667eea;
color: #667eea;
}
.btn-test .role-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.btn-test.user .role-badge {
background: #e3f2fd;
color: #1976d2;
}
.btn-test.admin .role-badge {
background: #fff3e0;
color: #f57c00;
}
.btn-test.superadmin .role-badge {
background: #fce4ec;
color: #c2185b;
}
.footer-note {
margin-top: 25px;
text-align: center;
font-size: 12px;
color: #999;
line-height: 1.6;
}
.icon {
width: 20px;
height: 20px;
fill: currentColor;
}
@media (max-width: 480px) {
.login-container {
margin: 10px;
}
.system-title {
font-size: 24px;
}
.login-body {
padding: 30px 20px;
}
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<div class="logo-container">
<img src="logo.svg" alt="System Logo">
</div>
<h1 class="system-title">那都AI寫的不要問我</h1>
<p class="system-subtitle">HR Position Management System</p>
</div>
<div class="login-body">
<form id="loginForm" onsubmit="handleLogin(event)">
<div class="form-group">
<label for="username">
<svg class="icon" viewBox="0 0 24 24" style="width: 16px; height: 16px; vertical-align: middle; margin-right: 5px;">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
工號 / 帳號
</label>
<input type="text" id="username" name="username" placeholder="請輸入工號" required>
</div>
<div class="form-group">
<label for="password">
<svg class="icon" viewBox="0 0 24 24" style="width: 16px; height: 16px; vertical-align: middle; margin-right: 5px;">
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
</svg>
密碼
</label>
<input type="password" id="password" name="password" placeholder="請輸入密碼" required>
</div>
<button type="submit" class="btn btn-primary">
<svg class="icon" viewBox="0 0 24 24">
<path d="M10 17l5-5-5-5v10z" fill="currentColor"/>
</svg>
登入系統
</button>
</form>
<div class="divider">
<span>或使用測試帳號快速登入</span>
</div>
<div class="quick-login">
<p class="quick-login-title">選擇測試角色</p>
<button type="button" class="btn btn-test user" onclick="quickLogin('user')">
<svg class="icon" viewBox="0 0 24 24">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
<span class="role-badge">一般使用者</span>
<span style="flex: 1; text-align: left; margin-left: 10px; font-size: 13px; color: #666;">A003 / employee</span>
</button>
<button type="button" class="btn btn-test admin" onclick="quickLogin('admin')">
<svg class="icon" viewBox="0 0 24 24">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>
</svg>
<span class="role-badge">管理者</span>
<span style="flex: 1; text-align: left; margin-left: 10px; font-size: 13px; color: #666;">A002 / hr_manager</span>
</button>
<button type="button" class="btn btn-test superadmin" onclick="quickLogin('superadmin')">
<svg class="icon" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
<span class="role-badge">最高管理者</span>
<span style="flex: 1; text-align: left; margin-left: 10px; font-size: 13px; color: #666;">A001 / admin</span>
</button>
</div>
<div class="footer-note">
這個系統真的都是 AI 寫的<br>
如果有問題... 那就是 AI 的問題 ¯\_(ツ)_/¯
</div>
</div>
</div>
<script>
// 測試帳號資料
const testAccounts = {
'user': {
username: 'A003',
password: 'employee',
role: 'user',
name: '一般員工'
},
'admin': {
username: 'A002',
password: 'hr_manager',
role: 'admin',
name: '人資主管'
},
'superadmin': {
username: 'A001',
password: 'admin',
role: 'superadmin',
name: '系統管理員'
}
};
// 處理一般登入
function handleLogin(event) {
event.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
// 簡單的驗證邏輯(實際應該調用後端 API
let validUser = null;
for (const [key, account] of Object.entries(testAccounts)) {
if (account.username === username && account.password === password) {
validUser = account;
break;
}
}
if (validUser) {
// 儲存登入資訊
localStorage.setItem('currentUser', JSON.stringify(validUser));
// 顯示登入成功訊息
showLoginSuccess(validUser.name, validUser.role);
// 延遲跳轉到主頁面
setTimeout(() => {
window.location.href = 'index.html';
}, 1500);
} else {
alert('帳號或密碼錯誤!\n\n提示您可以使用下方的測試帳號快速登入');
}
}
// 快速登入
function quickLogin(role) {
const account = testAccounts[role];
// 填入表單
document.getElementById('username').value = account.username;
document.getElementById('password').value = account.password;
// 儲存登入資訊
localStorage.setItem('currentUser', JSON.stringify(account));
// 顯示登入成功訊息
showLoginSuccess(account.name, account.role);
// 延遲跳轉到主頁面
setTimeout(() => {
window.location.href = 'index.html';
}, 1500);
}
// 顯示登入成功訊息
function showLoginSuccess(name, role) {
const roleNames = {
'user': '一般使用者',
'admin': '管理者',
'superadmin': '最高管理者'
};
// 創建成功訊息元素
const successDiv = document.createElement('div');
successDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 30px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
z-index: 9999;
animation: slideInRight 0.5s ease-out;
font-size: 16px;
`;
successDiv.innerHTML = `
<div style="display: flex; align-items: center; gap: 15px;">
<svg viewBox="0 0 24 24" style="width: 32px; height: 32px; fill: white;">
<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>
<div>
<div style="font-weight: 600; margin-bottom: 5px;">登入成功!</div>
<div style="font-size: 14px; opacity: 0.9;">${name} (${roleNames[role]})</div>
</div>
</div>
`;
// 添加動畫樣式
const style = document.createElement('style');
style.textContent = `
@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
`;
document.head.appendChild(style);
document.body.appendChild(successDiv);
}
// 頁面載入時檢查是否已登入
window.addEventListener('DOMContentLoaded', function() {
const currentUser = localStorage.getItem('currentUser');
if (currentUser) {
const user = JSON.parse(currentUser);
// 如果已經登入,詢問是否要繼續使用或重新登入
const confirm = window.confirm(`偵測到您已經以「${user.name}」身份登入。\n\n是否要繼續使用此帳號?\n(取消將重新登入)`);
if (confirm) {
window.location.href = 'index.html';
} else {
localStorage.removeItem('currentUser');
}
}
});
</script>
</body>
</html>

View File

@@ -1 +0,0 @@
{"error":"\u627e\u4e0d\u5230\u8a72\u5d17\u4f4d\u8cc7\u6599","success":false}
Can't render this file because it contains an unexpected character in line 1 and column 2.

2215
review.html Normal file

File diff suppressed because one or more lines are too long

26
scripts/check_models.py Normal file
View File

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

View File

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

View File

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

382
scripts/generate_review.py Normal file
View File

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

View File

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

255
scripts/rename_field_ids.py Normal file
View File

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

View File

@@ -32,8 +32,8 @@ cors_origins = os.getenv('CORS_ORIGINS', 'http://localhost:5000,http://127.0.0.1
CORS(app, origins=cors_origins) CORS(app, origins=cors_origins)
# 模擬資料庫 # 模擬資料庫
positions_db = {} HR_position_positions_db = {}
jobs_db = {} HR_position_jobs_db = {}
# 預設崗位資料 # 預設崗位資料
default_positions = { default_positions = {
@@ -76,7 +76,7 @@ default_positions = {
"updatedAt": "2024-01-01T00:00:00" "updatedAt": "2024-01-01T00:00:00"
} }
} }
positions_db.update(default_positions) HR_position_positions_db.update(default_positions)
# 預設職務資料 # 預設職務資料
default_jobs = { default_jobs = {
@@ -98,7 +98,7 @@ default_jobs = {
"updatedAt": "2024-01-01T00:00:00" "updatedAt": "2024-01-01T00:00:00"
} }
} }
jobs_db.update(default_jobs) HR_position_jobs_db.update(default_jobs)
# ==================== 靜態頁面 ==================== # ==================== 靜態頁面 ====================
@@ -118,7 +118,7 @@ def get_positions():
size = request.args.get('size', 20, type=int) size = request.args.get('size', 20, type=int)
search = request.args.get('search', '', type=str) search = request.args.get('search', '', type=str)
filtered = list(positions_db.values()) filtered = list(HR_position_positions_db.values())
if search: if search:
filtered = [p for p in filtered filtered = [p for p in filtered
if search.lower() in p['basicInfo'].get('positionCode', '').lower() if search.lower() in p['basicInfo'].get('positionCode', '').lower()
@@ -142,9 +142,9 @@ def get_positions():
@app.route('/api/positions/<position_id>', methods=['GET']) @app.route('/api/positions/<position_id>', methods=['GET'])
def get_position(position_id): def get_position(position_id):
if position_id not in positions_db: if position_id not in HR_position_positions_db:
return jsonify({'success': False, 'error': '找不到該崗位資料'}), 404 return jsonify({'success': False, 'error': '找不到該崗位資料'}), 404
return jsonify({'success': True, 'data': positions_db[position_id]}) return jsonify({'success': True, 'data': HR_position_positions_db[position_id]})
@app.route('/api/positions', methods=['POST']) @app.route('/api/positions', methods=['POST'])
def create_position(): def create_position():
@@ -160,7 +160,7 @@ def create_position():
return jsonify({'success': False, 'error': '崗位名稱為必填欄位'}), 400 return jsonify({'success': False, 'error': '崗位名稱為必填欄位'}), 400
position_code = basic_info['positionCode'] position_code = basic_info['positionCode']
if position_code in positions_db: if position_code in HR_position_positions_db:
return jsonify({'success': False, 'error': f'崗位編號 {position_code} 已存在'}), 409 return jsonify({'success': False, 'error': f'崗位編號 {position_code} 已存在'}), 409
now = datetime.now().isoformat() now = datetime.now().isoformat()
@@ -171,7 +171,7 @@ def create_position():
'createdAt': now, 'createdAt': now,
'updatedAt': now 'updatedAt': now
} }
positions_db[position_code] = new_position HR_position_positions_db[position_code] = new_position
return jsonify({'success': True, 'message': '崗位資料新增成功', 'data': new_position}), 201 return jsonify({'success': True, 'message': '崗位資料新增成功', 'data': new_position}), 201
except Exception as e: except Exception as e:
return jsonify({'success': False, 'error': f'新增失敗: {str(e)}'}), 500 return jsonify({'success': False, 'error': f'新增失敗: {str(e)}'}), 500

110
styles/base.css Normal file
View File

@@ -0,0 +1,110 @@
/**
* Base Styles - 基礎樣式
* 包含 CSS 變數定義、CSS Reset、全域樣式
*/
/* ==================== CSS Variables ==================== */
:root {
/* 主題色 */
--primary: #1a5276;
--primary-light: #2980b9;
--primary-dark: #0e3a53;
--accent: #e67e22;
--accent-light: #f39c12;
--green: #27ae60;
--green-dark: #1e8449;
/* 模組專屬色彩 */
/* 崗位基礎資料 - 藍色 (Primary) */
--module-position: #1a5276;
--module-position-dark: #0e3a53;
--module-position-shadow: rgba(26, 82, 118, 0.4);
/* 職務基礎資料 - 橘色 */
--module-job: #e67e22;
--module-job-dark: #d35400;
--module-job-shadow: rgba(230, 126, 34, 0.4);
/* 部門職責 - 紫色 */
--module-deptfunc: #8b5cf6;
--module-deptfunc-dark: #6d28d9;
--module-deptfunc-shadow: rgba(139, 92, 246, 0.4);
/* 崗位描述 - 綠色 */
--module-desc: #27ae60;
--module-desc-dark: #1e8449;
--module-desc-shadow: rgba(39, 174, 96, 0.4);
/* 崗位清單 - 青色 */
--module-list: #14b8a6;
--module-list-dark: #0d9488;
--module-list-shadow: rgba(20, 184, 166, 0.4);
/* 管理者頁面 - 玫紅色 */
--module-admin: #f43f5e;
--module-admin-dark: #e11d48;
--module-admin-shadow: rgba(244, 63, 94, 0.4);
/* 背景色 */
--bg: #f4f6f9;
--bg-main: #f4f6f9;
--bg-card: #ffffff;
--border: #d5dbdf;
/* 文字顏色 */
--text-primary: #2c3e50;
--text-secondary: #5d6d7e;
/* 語義化顏色 */
--success: #27ae60;
--warning: #f39c12;
--danger: #e74c3c;
/* 陰影與圓角 */
--shadow: 0 4px 20px rgba(26, 82, 118, 0.12);
--radius: 8px;
}
/* ==================== CSS Reset ==================== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* ==================== Global Styles ==================== */
body {
font-family: 'Noto Sans TC', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #e8f4fc 0%, #f4f6f9 100%);
min-height: 100vh;
color: var(--text-primary);
}
/* ==================== Animations ==================== */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes modalIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}

1154
styles/components.css Normal file

File diff suppressed because it is too large Load Diff

522
styles/layout.css Normal file
View File

@@ -0,0 +1,522 @@
/**
* Layout Styles - 布局樣式
* Two-Column Layout: 左側導航 + 右側內容
*/
/* ==================== Main Layout ==================== */
.app-container {
display: flex;
min-height: 100vh;
background: var(--bg);
}
/* ==================== Sidebar (左側導航欄) ==================== */
.sidebar {
width: 240px;
min-width: 240px;
background: linear-gradient(180deg, #1a1f2e 0%, #2d3748 100%);
display: flex;
flex-direction: column;
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 100;
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.15);
}
/* ==================== User Info (側邊欄頂部) ==================== */
.user-info-bar {
padding: 20px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
gap: 12px;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
color: #ffffff;
}
.user-avatar {
width: 44px;
height: 44px;
border-radius: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.1rem;
color: white;
}
.user-details {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.user-name {
font-weight: 600;
font-size: 0.95rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-role {
font-size: 0.75rem;
opacity: 0.7;
color: #a0aec0;
}
.logout-btn {
width: 100%;
padding: 10px 16px;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: #e2e8f0;
font-family: inherit;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s ease;
}
.logout-btn:hover {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.4);
color: #fca5a5;
}
.logout-btn svg {
width: 16px;
height: 16px;
fill: currentColor;
}
/* ==================== Module Selector (垂直導航選單) ==================== */
.module-selector {
display: flex;
flex-direction: column;
padding: 16px 12px;
gap: 6px;
flex: 1;
overflow-y: auto;
}
.module-btn {
width: 100%;
padding: 12px 14px;
border: none;
border-radius: 8px;
background: transparent;
font-family: inherit;
font-size: 0.875rem;
font-weight: 500;
color: #a0aec0;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s ease;
text-align: left;
}
.module-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: #e2e8f0;
}
.module-btn.active {
background: linear-gradient(135deg, var(--module-position) 0%, var(--module-position-dark) 100%);
color: white;
box-shadow: 0 4px 12px var(--module-position-shadow);
}
.module-btn.active.job-active {
background: linear-gradient(135deg, var(--module-job) 0%, var(--module-job-dark) 100%);
box-shadow: 0 4px 12px var(--module-job-shadow);
}
.module-btn.active.deptfunc-active {
background: linear-gradient(135deg, var(--module-deptfunc) 0%, var(--module-deptfunc-dark) 100%);
box-shadow: 0 4px 12px var(--module-deptfunc-shadow);
}
.module-btn.active.desc-active {
background: linear-gradient(135deg, var(--module-desc) 0%, var(--module-desc-dark) 100%);
box-shadow: 0 4px 12px var(--module-desc-shadow);
}
.module-btn.active.list-active {
background: linear-gradient(135deg, var(--module-list) 0%, var(--module-list-dark) 100%);
box-shadow: 0 4px 12px var(--module-list-shadow);
}
.module-btn.active.admin-active {
background: linear-gradient(135deg, var(--module-admin) 0%, var(--module-admin-dark) 100%);
box-shadow: 0 4px 12px var(--module-admin-shadow);
}
.module-btn svg {
width: 20px;
height: 20px;
fill: currentColor;
flex-shrink: 0;
}
/* ==================== Main Content (右側內容區) ==================== */
.main-content {
flex: 1;
margin-left: 240px;
padding: 24px 32px;
min-height: 100vh;
background: var(--bg);
}
.module-content {
display: none;
}
.module-content.active {
display: block;
animation: fadeIn 0.3s ease;
}
/* ==================== App Header ==================== */
.app-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding: 20px 28px;
background: linear-gradient(135deg, var(--module-position) 0%, var(--module-position-dark) 100%);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.app-header.job-header {
background: linear-gradient(135deg, var(--module-job) 0%, var(--module-job-dark) 100%);
}
.app-header.deptfunc-header {
background: linear-gradient(135deg, var(--module-deptfunc) 0%, var(--module-deptfunc-dark) 100%);
}
.app-header.desc-header {
background: linear-gradient(135deg, var(--module-desc) 0%, var(--module-desc-dark) 100%);
}
.app-header.list-header {
background: linear-gradient(135deg, var(--module-list) 0%, var(--module-list-dark) 100%);
}
.app-header.admin-header {
background: linear-gradient(135deg, var(--module-admin) 0%, var(--module-admin-dark) 100%);
}
.app-header .icon {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.15);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.app-header .icon svg {
width: 28px;
height: 28px;
fill: #ffffff;
}
.app-header h1 {
color: #ffffff;
font-size: 1.5rem;
font-weight: 600;
}
.app-header .subtitle {
color: rgba(255, 255, 255, 0.7);
font-size: 0.875rem;
margin-top: 2px;
}
/* ==================== Form Card ==================== */
.form-card {
background: var(--bg-card);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
/* ==================== Sections ==================== */
.section-box {
background: #f8fafc;
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 20px;
}
.section-header {
padding: 12px 16px;
background: linear-gradient(135deg, #e8f4fc 0%, #f0f4f8 100%);
border-bottom: 1px solid var(--border);
font-weight: 600;
font-size: 0.9rem;
color: var(--primary);
}
.section-header.green {
background: linear-gradient(135deg, #e8f8f0 0%, #f0f8f4 100%);
color: var(--green-dark);
}
.section-body {
padding: 20px;
}
.section-divider {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 12px;
margin: 8px 0;
}
.section-divider span {
font-size: 0.85rem;
font-weight: 600;
color: var(--primary);
white-space: nowrap;
}
.section-divider::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(to right, var(--border), transparent);
}
/* ==================== Sidebar Footer ==================== */
.sidebar-footer {
padding: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
font-size: 0.75rem;
color: #64748b;
text-align: center;
}
/* ==================== Responsive Design ==================== */
/* Large Tablets & Small Desktops (1024px) */
@media (max-width: 1024px) {
.sidebar {
width: 200px;
min-width: 200px;
}
.main-content {
margin-left: 200px;
padding: 20px 24px;
}
.app-header {
padding: 16px 20px;
}
.app-header h1 {
font-size: 1.3rem;
}
}
/* Tablets (768px) */
@media (max-width: 768px) {
.app-container {
flex-direction: column;
}
.sidebar {
position: relative;
width: 100%;
min-width: 100%;
flex-direction: row;
flex-wrap: wrap;
padding: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
}
.user-info-bar {
width: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 12px;
border-bottom: none;
}
.user-info {
flex: 1;
}
.logout-btn {
width: auto;
padding: 8px 16px;
}
.module-selector {
width: 100%;
flex-direction: row;
flex-wrap: wrap;
padding: 8px 0;
gap: 8px;
overflow-x: auto;
}
.module-btn {
flex: 1;
min-width: calc(33.33% - 8px);
padding: 10px;
justify-content: center;
font-size: 0.75rem;
}
.module-btn span {
display: none;
}
.main-content {
margin-left: 0;
padding: 16px;
}
.app-header {
padding: 16px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.app-header .icon {
width: 40px;
height: 40px;
}
.app-header .icon svg {
width: 22px;
height: 22px;
}
.app-header h1 {
font-size: 1.1rem;
}
.app-header .subtitle {
display: none;
}
.sidebar-footer {
display: none;
}
.section-body {
padding: 16px;
}
}
/* Mobile Phones (480px) */
@media (max-width: 480px) {
.sidebar {
padding: 8px;
}
.user-info-bar {
padding: 8px;
gap: 8px;
}
.user-avatar {
width: 36px;
height: 36px;
font-size: 0.9rem;
}
.user-name {
font-size: 0.85rem;
}
.user-role {
font-size: 0.7rem;
}
.logout-btn {
padding: 6px 12px;
font-size: 0.8rem;
}
.logout-btn span {
display: none;
}
.module-selector {
gap: 6px;
}
.module-btn {
min-width: calc(50% - 6px);
padding: 8px;
}
.module-btn svg {
width: 18px;
height: 18px;
}
.main-content {
padding: 12px;
}
.app-header {
padding: 12px;
gap: 12px;
}
.app-header .icon {
width: 36px;
height: 36px;
border-radius: 8px;
}
.app-header .icon svg {
width: 20px;
height: 20px;
}
.app-header h1 {
font-size: 1rem;
}
.section-box {
margin-bottom: 12px;
}
.section-header {
padding: 10px 12px;
font-size: 0.85rem;
}
.section-body {
padding: 12px;
}
}

336
styles/modules.css Normal file
View File

@@ -0,0 +1,336 @@
/**
* Modules Styles - 模組專屬樣式
* 包含各個頁籤/模組的特定樣式需求
*/
/* ==================== 共用模組樣式 ==================== */
/* 模組頂部工具列 */
.module-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 20px;
padding: 16px 20px;
background: #f8fafc;
border-radius: var(--radius);
border: 1px solid var(--border);
}
.module-toolbar-left,
.module-toolbar-right {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.module-toolbar h2 {
margin: 0;
color: var(--primary);
font-size: 1.1rem;
}
.module-toolbar .hint {
color: var(--text-secondary);
font-size: 0.85rem;
}
/* ==================== 崗位基礎資料模組 ==================== */
/* CSV 按鈕區塊 */
.csv-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 15px;
}
/* ==================== 職務基礎資料模組 ==================== */
/* (使用共用元件) */
/* ==================== 部門職責模組 ==================== */
/* (使用共用元件) */
/* ==================== 崗位描述模組 ==================== */
/* (使用共用元件) */
/* ==================== 崗位清單模組 ==================== */
.position-list-module .module-toolbar {
background: linear-gradient(135deg, rgba(20, 184, 166, 0.05) 0%, rgba(13, 148, 136, 0.05) 100%);
border-color: rgba(20, 184, 166, 0.2);
}
/* 資料表格容器 */
.data-table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border-radius: var(--radius);
border: 1px solid var(--border);
}
/* 資料表格 */
.data-table {
width: 100%;
border-collapse: collapse;
background: white;
min-width: 700px;
}
.data-table thead {
position: sticky;
top: 0;
z-index: 10;
}
.data-table th {
padding: 14px 16px;
text-align: left;
font-weight: 600;
font-size: 0.85rem;
white-space: nowrap;
background: var(--primary);
color: white;
border-bottom: 2px solid var(--primary-dark);
}
.data-table th.sortable {
cursor: pointer;
user-select: none;
transition: background 0.2s ease;
}
.data-table th.sortable:hover {
background: var(--primary-dark);
}
.data-table th .sort-icon {
margin-left: 6px;
opacity: 0.7;
}
.data-table td {
padding: 12px 16px;
font-size: 0.9rem;
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
.data-table tbody tr:hover {
background: rgba(41, 128, 185, 0.05);
}
.data-table tbody tr:last-child td {
border-bottom: none;
}
/* 空資料提示 */
.data-table .empty-row td {
padding: 48px 20px;
text-align: center;
color: var(--text-secondary);
font-size: 0.95rem;
}
/* 操作按鈕欄 */
.data-table .action-cell {
text-align: center;
white-space: nowrap;
}
.data-table .action-cell .btn {
padding: 6px 12px;
font-size: 0.8rem;
margin: 0 2px;
}
/* 崗位清單專用表格顏色 - 青色 */
.position-list-table thead {
background: linear-gradient(135deg, var(--module-list) 0%, var(--module-list-dark) 100%);
}
.position-list-table th {
background: transparent;
border-bottom-color: var(--module-list-dark);
}
.position-list-table th.sortable:hover {
background: rgba(0, 0, 0, 0.1);
}
/* ==================== 管理者頁面模組 ==================== */
.admin-module .module-toolbar {
background: linear-gradient(135deg, rgba(244, 63, 94, 0.05) 0%, rgba(225, 29, 72, 0.05) 100%);
border-color: rgba(244, 63, 94, 0.2);
}
/* 管理者表格專用顏色 - 玫紅色 */
.admin-table thead {
background: linear-gradient(135deg, var(--module-admin) 0%, var(--module-admin-dark) 100%);
}
.admin-table th {
background: transparent;
border-bottom-color: var(--module-admin-dark);
}
/* 權限標籤 */
.permission-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
}
.permission-badge.admin-highest {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
}
.permission-badge.admin {
background: linear-gradient(135deg, #f39c12 0%, #d68910 100%);
color: white;
}
.permission-badge.user {
background: linear-gradient(135deg, #27ae60 0%, #1e8449 100%);
color: white;
}
.permission-badge.viewer {
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
color: white;
}
/* ==================== 響應式設計 - 模組專屬 ==================== */
/* Large Tablets (1024px) */
@media (max-width: 1024px) {
.module-toolbar {
padding: 14px 16px;
}
.data-table {
min-width: 600px;
}
.data-table th,
.data-table td {
padding: 12px 14px;
}
}
/* Tablets (768px) */
@media (max-width: 768px) {
.module-toolbar {
flex-direction: column;
align-items: stretch;
gap: 12px;
padding: 12px;
}
.module-toolbar-left,
.module-toolbar-right {
justify-content: center;
}
.module-toolbar h2 {
text-align: center;
font-size: 1rem;
}
.module-toolbar .hint {
display: none;
}
.csv-buttons {
justify-content: center;
}
.data-table {
min-width: 500px;
}
.data-table th,
.data-table td {
padding: 10px 12px;
font-size: 0.85rem;
}
.data-table .action-cell .btn {
padding: 5px 10px;
font-size: 0.75rem;
}
/* 隱藏較不重要的欄位 */
.data-table .hide-tablet {
display: none;
}
.permission-badge {
padding: 3px 8px;
font-size: 0.7rem;
}
}
/* Mobile Phones (480px) */
@media (max-width: 480px) {
.module-toolbar {
padding: 10px;
gap: 10px;
}
.module-toolbar-left,
.module-toolbar-right {
width: 100%;
flex-direction: column;
}
.module-toolbar-left .btn,
.module-toolbar-right .btn {
width: 100%;
justify-content: center;
}
.csv-buttons {
flex-direction: column;
}
.csv-buttons .btn {
width: 100%;
justify-content: center;
}
.data-table {
min-width: 400px;
}
.data-table th,
.data-table td {
padding: 8px 10px;
font-size: 0.8rem;
}
.data-table th {
font-size: 0.75rem;
}
.data-table .action-cell {
display: flex;
flex-direction: column;
gap: 4px;
}
.data-table .action-cell .btn {
width: 100%;
padding: 6px 8px;
}
/* 隱藏更多欄位 */
.data-table .hide-mobile {
display: none;
}
}

244
styles/utilities.css Normal file
View File

@@ -0,0 +1,244 @@
/**
* Utilities Styles - 工具類別與額外響應式設計
* 包含快速工具類別、表格響應式、額外斷點調整
*/
/* ==================== Utility Classes ==================== */
/* Display */
.d-none { display: none !important; }
.d-block { display: block !important; }
.d-flex { display: flex !important; }
.d-grid { display: grid !important; }
/* Flexbox */
.flex-wrap { flex-wrap: wrap !important; }
.flex-column { flex-direction: column !important; }
.justify-center { justify-content: center !important; }
.justify-between { justify-content: space-between !important; }
.align-center { align-items: center !important; }
.gap-1 { gap: 4px !important; }
.gap-2 { gap: 8px !important; }
.gap-3 { gap: 12px !important; }
.gap-4 { gap: 16px !important; }
/* Margin */
.mt-0 { margin-top: 0 !important; }
.mt-1 { margin-top: 4px !important; }
.mt-2 { margin-top: 8px !important; }
.mt-3 { margin-top: 12px !important; }
.mt-4 { margin-top: 16px !important; }
.mb-0 { margin-bottom: 0 !important; }
.mb-1 { margin-bottom: 4px !important; }
.mb-2 { margin-bottom: 8px !important; }
.mb-3 { margin-bottom: 12px !important; }
.mb-4 { margin-bottom: 16px !important; }
/* Padding */
.p-0 { padding: 0 !important; }
.p-1 { padding: 4px !important; }
.p-2 { padding: 8px !important; }
.p-3 { padding: 12px !important; }
.p-4 { padding: 16px !important; }
/* Text */
.text-center { text-align: center !important; }
.text-left { text-align: left !important; }
.text-right { text-align: right !important; }
.text-sm { font-size: 0.875rem !important; }
.text-xs { font-size: 0.75rem !important; }
.text-muted { color: var(--text-secondary) !important; }
.text-primary { color: var(--primary) !important; }
.text-danger { color: var(--danger) !important; }
.text-success { color: var(--success) !important; }
.fw-bold { font-weight: 600 !important; }
.fw-normal { font-weight: 400 !important; }
/* Width */
.w-100 { width: 100% !important; }
.w-auto { width: auto !important; }
/* Overflow */
.overflow-auto { overflow: auto !important; }
.overflow-hidden { overflow: hidden !important; }
.overflow-x-auto { overflow-x: auto !important; }
/* Visibility */
.visible { visibility: visible !important; }
.invisible { visibility: hidden !important; }
/* ==================== Table Responsive ==================== */
.table-responsive {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-responsive table {
min-width: 600px;
}
/* ==================== Position List Specific ==================== */
.position-table {
width: 100%;
border-collapse: collapse;
}
.position-table th,
.position-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border);
}
.position-table th {
background: #f8fafc;
font-weight: 600;
color: var(--text-secondary);
font-size: 0.85rem;
white-space: nowrap;
}
.position-table td {
font-size: 0.9rem;
}
.position-table tr:hover {
background: rgba(41, 128, 185, 0.05);
}
/* ==================== Responsive Hide/Show ==================== */
@media (min-width: 769px) {
.hide-desktop { display: none !important; }
}
@media (max-width: 768px) {
.hide-tablet { display: none !important; }
}
@media (max-width: 480px) {
.hide-mobile { display: none !important; }
}
@media (min-width: 481px) {
.show-mobile-only { display: none !important; }
}
/* ==================== Extra Responsive Adjustments ==================== */
/* Tablets (768px) */
@media (max-width: 768px) {
/* Table */
.position-table th,
.position-table td {
padding: 10px 12px;
font-size: 0.85rem;
}
/* Utility overrides */
.text-sm { font-size: 0.8rem !important; }
.text-xs { font-size: 0.7rem !important; }
/* Form wrapper for side-by-side elements */
.input-with-button {
flex-direction: column;
}
.input-with-button .btn-lookup {
width: 100%;
justify-content: center;
}
}
/* Mobile Phones (480px) */
@media (max-width: 480px) {
/* Table */
.position-table th,
.position-table td {
padding: 8px 10px;
font-size: 0.8rem;
}
/* Stack buttons on very small screens */
.btn-group-vertical {
flex-direction: column;
width: 100%;
}
.btn-group-vertical .btn {
width: 100%;
}
/* Hide less important table columns */
.position-table .hide-xs {
display: none;
}
}
/* ==================== Print Styles ==================== */
@media print {
.sidebar,
.logout-btn,
.module-selector,
.form-actions,
.ai-bags-container,
.ai-generate-btn,
.nav-buttons,
.toast,
.modal-overlay {
display: none !important;
}
.main-content {
margin-left: 0 !important;
padding: 0 !important;
}
.app-container {
display: block;
}
.form-card {
box-shadow: none;
border: 1px solid #ddd;
}
.tab-content {
display: block !important;
page-break-inside: avoid;
}
.tabs {
display: none;
}
}
/* ==================== Safe Area for notched devices ==================== */
@supports (padding: max(0px)) {
.sidebar {
padding-left: max(12px, env(safe-area-inset-left));
padding-right: max(12px, env(safe-area-inset-right));
}
.main-content {
padding-left: max(16px, env(safe-area-inset-left));
padding-right: max(16px, env(safe-area-inset-right));
}
@media (max-width: 768px) {
.sidebar {
padding-bottom: max(12px, env(safe-area-inset-bottom));
}
}
}
/* ==================== Reduced Motion ==================== */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

View File

@@ -0,0 +1,62 @@
"""
Test deepseek-reasoner model on Ollama API
"""
import requests
import json
import urllib3
import sys
import codecs
# Set UTF-8 encoding for output
if sys.platform == 'win32':
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
# Disable SSL warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://ollama_pjapi.theaken.com"
print("=" * 60)
print("Testing deepseek-reasoner model")
print("=" * 60)
print()
# Test chat completion with deepseek-reasoner
print("Sending test prompt to deepseek-reasoner...")
try:
chat_request = {
"model": "deepseek-reasoner",
"messages": [
{"role": "user", "content": "請用中文簡單地說明什麼是人工智慧"}
]
}
response = requests.post(
f"{API_URL}/v1/chat/completions",
json=chat_request,
headers={'Content-Type': 'application/json'},
timeout=60,
verify=False
)
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
result = response.json()
text = result['choices'][0]['message']['content']
print("\nResponse:")
print("-" * 60)
print(text)
print("-" * 60)
# Save to file
with open('deepseek_reasoner_output.txt', 'w', encoding='utf-8') as f:
f.write(text)
print("\n✓ Response saved to: deepseek_reasoner_output.txt")
else:
print(f"Error: {response.text}")
except Exception as e:
print(f"Error: {str(e)}")
print()
print("=" * 60)

70
tests/test_ollama.py Normal file
View File

@@ -0,0 +1,70 @@
"""
Test Ollama API integration
"""
import requests
import json
import urllib3
# Disable SSL warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://ollama_pjapi.theaken.com"
print("=" * 60)
print("Testing Ollama API Connection")
print("=" * 60)
print()
# Test 1: List models
print("Test 1: Listing available models...")
try:
response = requests.get(f"{API_URL}/v1/models", timeout=10, verify=False)
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
data = response.json()
models = data.get('data', [])
print(f"Found {len(models)} models:")
for model in models[:5]:
print(f" - {model.get('id', 'Unknown')}")
else:
print(f"Error: {response.text}")
except Exception as e:
print(f"Error: {str(e)}")
print()
# Test 2: Chat completion
print("Test 2: Testing chat completion...")
try:
chat_request = {
"model": "qwen2.5:3b",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Say hello in Chinese."}
],
"temperature": 0.7,
"max_tokens": 50
}
response = requests.post(
f"{API_URL}/v1/chat/completions",
json=chat_request,
headers={'Content-Type': 'application/json'},
timeout=60,
verify=False
)
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
result = response.json()
text = result['choices'][0]['message']['content']
print(f"Response: {text}")
else:
print(f"Error: {response.text}")
except Exception as e:
print(f"Error: {str(e)}")
print()
print("=" * 60)

79
tests/test_ollama2.py Normal file
View File

@@ -0,0 +1,79 @@
"""
Test Ollama API with different parameters
"""
import requests
import json
import urllib3
# Disable SSL warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://ollama_pjapi.theaken.com"
print("=" * 60)
print("Testing Ollama Chat Completion - Variant Tests")
print("=" * 60)
print()
# Test 1: Using qwen2.5:72b (actual available model)
print("Test 1: Using qwen2.5:72b model...")
try:
chat_request = {
"model": "qwen2.5:72b",
"messages": [
{"role": "user", "content": "Say hello in Chinese."}
]
}
response = requests.post(
f"{API_URL}/v1/chat/completions",
json=chat_request,
headers={'Content-Type': 'application/json'},
timeout=60,
verify=False
)
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
result = response.json()
text = result['choices'][0]['message']['content']
print(f"Success! Response: {text}")
else:
print(f"Error: {response.text}")
except Exception as e:
print(f"Error: {str(e)}")
print()
# Test 2: Try deepseek-chat model
print("Test 2: Using deepseek-chat model...")
try:
chat_request = {
"model": "deepseek-chat",
"messages": [
{"role": "user", "content": "Say hello in Chinese."}
]
}
response = requests.post(
f"{API_URL}/v1/chat/completions",
json=chat_request,
headers={'Content-Type': 'application/json'},
timeout=60,
verify=False
)
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
result = response.json()
text = result['choices'][0]['message']['content']
print(f"Success! Response: {text}")
else:
print(f"Error: {response.text}")
except Exception as e:
print(f"Error: {str(e)}")
print()
print("=" * 60)

110
tests/test_ollama_final.py Normal file
View File

@@ -0,0 +1,110 @@
"""
Final Ollama API Integration Test
Tests the integration with the Flask app
"""
import requests
import json
import sys
# Set UTF-8 encoding for output
if sys.platform == 'win32':
import codecs
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
print("=" * 60)
print("Ollama API Integration Test (via Flask App)")
print("=" * 60)
print()
# Test 1: Test Ollama connection status
print("Test 1: Checking Ollama API configuration...")
try:
response = requests.get("http://localhost:5000/api/llm/config", timeout=10)
if response.status_code == 200:
config = response.json()
ollama_config = config.get('ollama', {})
print(f" Name: {ollama_config.get('name', 'N/A')}")
print(f" Enabled: {ollama_config.get('enabled', False)}")
print(f" Endpoint: {ollama_config.get('endpoint', 'N/A')}")
print(" Status: ✓ Configuration OK")
else:
print(f" Status: ✗ Error {response.status_code}")
except Exception as e:
print(f" Status: ✗ Error: {str(e)}")
print()
# Test 2: Generate text using Ollama
print("Test 2: Testing text generation with Ollama...")
try:
payload = {
"api": "ollama",
"prompt": "請用中文回答:你好嗎?",
"max_tokens": 100
}
response = requests.post(
"http://localhost:5000/api/llm/generate",
json=payload,
headers={'Content-Type': 'application/json'},
timeout=60
)
print(f" Status Code: {response.status_code}")
result = response.json()
if result.get('success'):
text = result.get('text', '')
print(f" Status: ✓ Generation successful")
print(f" Response length: {len(text)} characters")
print(f" Response preview: {text[:100]}...")
# Save full response to file
with open('ollama_response.txt', 'w', encoding='utf-8') as f:
f.write(text)
print(f" Full response saved to: ollama_response.txt")
else:
error = result.get('error', 'Unknown error')
print(f" Status: ✗ Generation failed")
print(f" Error: {error}")
except Exception as e:
print(f" Status: ✗ Error: {str(e)}")
print()
# Test 3: Test with English prompt
print("Test 3: Testing with English prompt...")
try:
payload = {
"api": "ollama",
"prompt": "Write a haiku about coding.",
"max_tokens": 100
}
response = requests.post(
"http://localhost:5000/api/llm/generate",
json=payload,
headers={'Content-Type': 'application/json'},
timeout=60
)
result = response.json()
if result.get('success'):
text = result.get('text', '')
print(f" Status: ✓ Generation successful")
print(f" Response:\n{text}")
else:
error = result.get('error', 'Unknown error')
print(f" Status: ✗ Generation failed")
print(f" Error: {error}")
except Exception as e:
print(f" Status: ✗ Error: {str(e)}")
print()
print("=" * 60)
print("Integration test completed!")
print("=" * 60)

View File

@@ -1,314 +0,0 @@
<EFBFBD>Ʒ~<7E><>,<EFBFBD>B<EFBFBD>O,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>W<EFBFBD><EFBFBD>,<EFBFBD>^<5E><><EFBFBD>W<EFBFBD><57>
<EFBFBD>b<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E>s,<EFBFBD>b<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E>s,<EFBFBD>b<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E>s,<EFBFBD><EFBFBD><EFBFBD>B<EFBFBD><EFBFBD>
,,,<EFBFBD><EFBFBD><EFBFBD>B<EFBFBD><EFBFBD><EFBFBD>U<EFBFBD>z
<EFBFBD>T<EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E><>,<EFBFBD>T<EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E><>,<EFBFBD>T<EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z
,,,<EFBFBD>M<EFBFBD>׸g<EFBFBD>z
<EFBFBD>k<EFBFBD>ȫ<EFBFBD>,<EFBFBD>k<EFBFBD>ȫ<EFBFBD>,<EFBFBD>k<EFBFBD>ȫ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>k<EFBFBD>ȱM<EFBFBD><EFBFBD>
,,,<EFBFBD>M<EFBFBD>Q<EFBFBD>u<EFBFBD>{<7B>v
<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>s<EFBFBD>y<EFBFBD>Ʒ~<7E><>,<EFBFBD>Ͳ<EFBFBD><EFBFBD>B,<EFBFBD>Ͳ<EFBFBD><EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD>Ͳ<EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>Ͳ<EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>ժ<EFBFBD>
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
,,,<EFBFBD>@<40>~<7E><>
,,<EFBFBD>Ͳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>s<EFBFBD>y<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>~<7E><><EFBFBD>ި,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>ʸ˫~<7E><><EFBFBD>ި<EFBFBD><DEA8><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,,<EFBFBD>ժ<EFBFBD>
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
,,,<EFBFBD>@<40>~<7E><>
,,<EFBFBD>~<7E><><EFBFBD>ި<EFBFBD><DEA8><EFBFBD><EFBFBD>X<EFBFBD><58>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>s<EFBFBD>y<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z
,,,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z<EFBFBD>U<EFBFBD>z
,<EFBFBD>ʸˤu<EFBFBD>{<7B>B,<EFBFBD>ʸˤu<EFBFBD>{<7B>B,<EFBFBD>B<EFBFBD><EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>s<EFBFBD>{<7B>u<EFBFBD>{<7B>@<40><>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>s<EFBFBD>{<7B>u<EFBFBD>{<7B>G<EFBFBD><47>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>W<EFBFBD><EFBFBD><EFBFBD>Z<EFBFBD>u<EFBFBD>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>I<EFBFBD><EFBFBD><EFBFBD>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD><EFBFBD><EFBFBD>L~<7E>s<EFBFBD>}<7D>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>q<EFBFBD><EFBFBD><EFBFBD>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>]<5D>Ƥ@<40><>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>]<5D>ƤG<C6A4><47>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>]<5D>Ƥ@<40><>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>]<5D>ƤG<C6A4><47>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>]<5D>ƤT<C6A4><54>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,<EFBFBD><EFBFBD><EFBFBD>`<60><EFBFBD><ECA4BD>,<EFBFBD>u<EFBFBD>~<7E>u<EFBFBD>{<7B><>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>u<EFBFBD>~<7E>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>M<EFBFBD>׺޲z,<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
,<EFBFBD><EFBFBD><EFBFBD>դu<EFBFBD>{<7B>P<EFBFBD><50><EFBFBD>o<EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD>դu<EFBFBD>{<7B>P<EFBFBD><50><EFBFBD>o<EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD><EFBFBD>դu<EFBFBD>{<7B><>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>]<5D>ƽ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD><EFBFBD><EFBFBD>ս<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>s<EFBFBD><EFBFBD><EFBFBD>~<7E>ɤJ<C9A4><4A>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD><EFBFBD><EFBFBD>o<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>ʸ˧޳N<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>]<5D>p<EFBFBD><70><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD><EFBFBD>ʳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD><EFBFBD><EFBFBD>ʤ@<40><>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD><EFBFBD>ʤG<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD>~<7E><><EFBFBD><EFBFBD><EAB7BD>,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD>ͺ޳<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>Ͳ<EFBFBD><EFBFBD>Ƶ{<7B><>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD><EFBFBD>~<7E><>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
,,,<EFBFBD>@<40>~<7E><>
,,<EFBFBD><EFBFBD>Ʊ<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD><EFBFBD><EFBFBD>Ʊ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD>ƭ<EFBFBD>,<EFBFBD>Z<EFBFBD><EFBFBD>
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
,,,<EFBFBD>@<40>~<7E><>
,<EFBFBD>t<EFBFBD>ȻP<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD>ú޲z<EFBFBD>B,<EFBFBD>t<EFBFBD>ȻP<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD>ú޲z<EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>t<EFBFBD>ȳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>t<EFBFBD>Ƚ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>w<EFBFBD>ä<EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD><EFBFBD>ʤƽ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E><>,<EFBFBD>B<EFBFBD><EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>i<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E>B,<EFBFBD><EFBFBD><EFBFBD>i<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E>B,<EFBFBD>B<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>޲z<DEB2><7A>(APD),<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>u<EFBFBD>{(APD),<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>޲z(APD),<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E>B,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E>B,<EFBFBD>B<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>޲z<DEB2><7A>(MPD),<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>u<EFBFBD>{(MPD),<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>M<EFBFBD>׸g<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>޲z(MPD),<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>M<EFBFBD>׸g<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>t,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>t,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>t,<EFBFBD>U<EFBFBD><EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD>~<7E>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,,<EFBFBD>@<40>~<7E><>
,,<EFBFBD>s<EFBFBD>y<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
,,,<EFBFBD>@<40>~<7E><>
,,<EFBFBD>t<EFBFBD>ȳ<EFBFBD>(Fab3),<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
,<EFBFBD>s<EFBFBD>{<7B>u<EFBFBD>{<7B>B,<EFBFBD>u<EFBFBD>{<7B>@<40><>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>u<EFBFBD>{<7B>G<EFBFBD><47>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>u<EFBFBD>{<7B>T<EFBFBD><54>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>s<EFBFBD>{<7B><><EFBFBD>X<EFBFBD><58>(Fab3),<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
<EFBFBD><EFBFBD><EFBFBD>ΤH<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>F<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>ΤH<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>F<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>ΤH<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>F<EFBFBD>Ʒ~<7E><>,<EFBFBD>H<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD><EFBFBD>F<EFBFBD>`<60>Ⱥ޲z<DEB2><7A>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>U<EFBFBD>z
,,<EFBFBD>۶ҥ<EFBFBD><EFBFBD>γ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD>V<EFBFBD>m<EFBFBD>o<EFBFBD>i<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD>~<7E>S<EFBFBD>޲z<DEB2><7A>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>M<EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD>ΰ]<5D>ȨƷ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>ΰ]<5D>ȨƷ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>ΰ]<5D>ȨƷ~<7E><>,<EFBFBD>]<5D>Ȫ<EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>j<EFBFBD>Z<EFBFBD>]<5D>ȳB,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>j<EFBFBD>Z<EFBFBD>]<5D>ȳB,<EFBFBD>B<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>j<EFBFBD>Z<EFBFBD>]<5D>ȳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>j<EFBFBD>Z<EFBFBD>]<5D>Ƚ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>ΰ]<5D>ȨƷ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>Χ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>H<EFBFBD><EFBFBD><EFBFBD>Y,<EFBFBD>M<EFBFBD>װƲz
<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>Ʒ~<7E><>,<EFBFBD>|<7C>p<EFBFBD><70>
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD>|<7C>p<EFBFBD><70>,<EFBFBD>g<EFBFBD>Ʋz
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD>|<7C>p<EFBFBD><70>,<EFBFBD>Ҫ<EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,,<EFBFBD>M<EFBFBD><EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD>|<7C>Ƚ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,,<EFBFBD>M<EFBFBD><EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD>޲z<EFBFBD>|<7C>p<EFBFBD><70>,<EFBFBD>g<EFBFBD>Ʋz
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD>ʸ˺޲z<EFBFBD>|<7C>p<EFBFBD><70>,<EFBFBD>Ҫ<EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,,<EFBFBD>M<EFBFBD><EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>޲z<EFBFBD>|<7C>p<EFBFBD><70>,<EFBFBD>Ҫ<EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,,<EFBFBD>M<EFBFBD><EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD>ΦX<EFBFBD>ֳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>B,,<EFBFBD>M<EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD>θ<EFBFBD><EFBFBD>T<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>θ<EFBFBD><EFBFBD>T<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>θ<EFBFBD><EFBFBD>T<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD><EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD><EFBFBD><EFBFBD>ʤp<EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>@<40>B,<EFBFBD><EFBFBD><EFBFBD>Ψt<EFBFBD>γ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>@<40>B,,<EFBFBD>u<EFBFBD>{<7B>v
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>@<40>B,<EFBFBD>q<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>X<EFBFBD>s<EFBFBD>y<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>@<40>B,,<EFBFBD>u<EFBFBD>{<7B>v
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>@<40>B,<EFBFBD>t<EFBFBD>κ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD>ȳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>@<40>B,,<EFBFBD>u<EFBFBD>{<7B>v
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>G<EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>G<EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
<EFBFBD>s<EFBFBD>ШƷ~<7E><>,<EFBFBD>s<EFBFBD>ШƷ~<7E><>,<EFBFBD>s<EFBFBD>ШƷ~<7E><>,<EFBFBD>B<EFBFBD><EFBFBD>
,<EFBFBD>s<EFBFBD>ШƷ~<7E><>,<EFBFBD><EFBFBD>޲z<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,<EFBFBD>s<EFBFBD>ШƷ~<7E><>,,<EFBFBD>M<EFBFBD><EFBFBD>
,<EFBFBD><EFBFBD><EFBFBD>o<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD>C<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>~<7E><><EFBFBD>o<EFBFBD>B,<EFBFBD>g<EFBFBD>Ʋz
,<EFBFBD><EFBFBD><EFBFBD>o<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,<EFBFBD>u<EFBFBD>{<7B>v
,<EFBFBD><EFBFBD><EFBFBD>o<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>~<7E><><EFBFBD>o<EFBFBD>B,<EFBFBD>g<EFBFBD>Ʋz
,<EFBFBD><EFBFBD><EFBFBD>o<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,<EFBFBD>u<EFBFBD>{<7B>v
<EFBFBD>]<5D>֫<EFBFBD>,<EFBFBD>]<5D>֫<EFBFBD>,<EFBFBD>]<5D>֫<EFBFBD>,<EFBFBD>D<EFBFBD><EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
<EFBFBD>`<60>g<EFBFBD>z<EFBFBD><7A>,<EFBFBD>`<60>g<EFBFBD>z<EFBFBD><7A>,<EFBFBD>`<60>g<EFBFBD>z<EFBFBD><7A>,<EFBFBD>`<60><>
,,,<EFBFBD>`<60>g<EFBFBD>z
,ESG<EFBFBD>M<EFBFBD>׿줽<EFBFBD><EFBFBD>,ESG<EFBFBD>M<EFBFBD>׿줽<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD><EFBFBD><EFBFBD>ҥ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>p<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>/<2F>u<EFBFBD>{<7B>v
,,<EFBFBD><EFBFBD><EFBFBD>|<7C><><EFBFBD><EFBFBD><EFBFBD>p<EFBFBD><70>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>/<2F>u<EFBFBD>{<7B>v
,,<EFBFBD><EFBFBD><EFBFBD>q<EFBFBD>v<EFBFBD>z<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>p<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>/<2F>u<EFBFBD>{<7B>v
,<EFBFBD>M<EFBFBD>׺޲z<EFBFBD><EFBFBD>,<EFBFBD>M<EFBFBD>׺޲z<EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z
,,<EFBFBD>M<EFBFBD>׺޲z,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>M<EFBFBD><EFBFBD>/<2F>u<EFBFBD>{<7B>v
,,PVS<EFBFBD>M<EFBFBD>׽<EFBFBD>,<EFBFBD>M<EFBFBD><EFBFBD>/<2F>u<EFBFBD>{<7B>v
<EFBFBD>`<60>~<7E><><EFBFBD>Ʒ~<7E><>,<EFBFBD>`<60>~<7E><><EFBFBD>Ʒ~<7E><>,<EFBFBD>`<60>~<7E><><EFBFBD>Ʒ~<7E><>,<EFBFBD>B<EFBFBD><EFBFBD>
,,<EFBFBD>Ȥ<EFBFBD><EFBFBD>~<7E><><EFBFBD>޲z<DEB2><7A>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>Ȥ<EFBFBD><EFBFBD>~<7E><><EFBFBD>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD>~<7E><><EFBFBD>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>~<7E><><EFBFBD>޲z<DEB2><7A>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>s<EFBFBD><EFBFBD><EFBFBD>~<7E>~<7E><><EFBFBD>޲z<DEB2><7A>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>ܧ<EFBFBD><EFBFBD>޲z<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>Ȥ<EFBFBD><EFBFBD><EFBFBD>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
,,,<EFBFBD>@<40>~<7E><>
,,<EFBFBD>~<7E><><EFBFBD>t<EFBFBD>ΤΫȤ<CEAB><C8A4>u<EFBFBD>{<7B><><EFBFBD>X<EFBFBD><58>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>Ȥ<EFBFBD><EFBFBD>u<EFBFBD>{<7B><><EFBFBD>X<EFBFBD><58>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>~<7E><><EFBFBD>t<EFBFBD>ν<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>ʴ<EFBFBD><EFBFBD>~<7E>]<5D>~<7E><><EFBFBD>޲z<DEB2><7A>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>~<7E><><EFBFBD>O<EFBFBD>ҳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD><EFBFBD><EFBFBD>Ĥ<EFBFBD><EFBFBD>R<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӻ޲z<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
,,,<EFBFBD>@<40>~<7E><>
,,<EFBFBD>H<EFBFBD><EFBFBD><EFBFBD>ʫO<EFBFBD>ҽ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
,,,<EFBFBD>@<40>~<7E><>
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӻ޲z<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z
,,,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z<EFBFBD>U<EFBFBD>z
,<EFBFBD>ӷ~<7E>}<7D>o<EFBFBD>[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>γB,<EFBFBD>ӷ~<7E>}<7D>o<EFBFBD>[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>γB,<EFBFBD>B<EFBFBD><EFBFBD>
,,,<EFBFBD>g<EFBFBD>z
,,,<EFBFBD>u<EFBFBD>{<7B>v
,<EFBFBD><EFBFBD><EFBFBD>~<7E>P<EFBFBD><50><EFBFBD>Ʒ~<7E>B,<EFBFBD><EFBFBD><EFBFBD>~<7E>P<EFBFBD><50><EFBFBD>Ʒ~<7E>B,<EFBFBD>B<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD>Ϻ[<5B>N<EFBFBD>u<EFBFBD>~<7E>ȳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>U<EFBFBD>z
,,<EFBFBD>N<EFBFBD>u,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>U<EFBFBD>z
,,<EFBFBD>ڨȰϷ~<7E>ȳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>U<EFBFBD>z
,,<EFBFBD>ڬw,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>U<EFBFBD>z
,,<EFBFBD>n<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>U<EFBFBD>z
,,<EFBFBD>F<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>U<EFBFBD>z
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϸ~<7E>ȳ<EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>U<EFBFBD>z
,,<EFBFBD><EFBFBD><EFBFBD>y<EFBFBD><EFBFBD><EFBFBD>ϫȤ<EFBFBD><EFBFBD>޲z,<EFBFBD>M<EFBFBD>׸g<EFBFBD>z
,,,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD>Ϸ~<7E>ȳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>U<EFBFBD>z
,<EFBFBD><EFBFBD><EFBFBD>y<EFBFBD>޳N<EFBFBD>A<EFBFBD>ȳB,<EFBFBD><EFBFBD><EFBFBD>y<EFBFBD>޳N<EFBFBD>A<EFBFBD>ȳB,<EFBFBD>B<EFBFBD><EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,,<EFBFBD>U<EFBFBD>z
,,<EFBFBD><EFBFBD><EFBFBD>Τu<EFBFBD>{<7B><>(GTS),<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>M<EFBFBD>׸g<EFBFBD>Ʋz
,,,<EFBFBD>޳N<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>t<EFBFBD>Τu<EFBFBD>{<7B><>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>u<EFBFBD>{<7B>v
,,<EFBFBD>S<EFBFBD>ʴ<EFBFBD><EFBFBD>ճ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>S<EFBFBD>ʴ<EFBFBD><EFBFBD>ս<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>u<EFBFBD>{<7B>v
,<EFBFBD><EFBFBD><EFBFBD>y<EFBFBD><EFBFBD><EFBFBD>P<EFBFBD>[<5B>~<7E>Ȥ䴩<C8A4>B,<EFBFBD><EFBFBD><EFBFBD>y<EFBFBD><EFBFBD><EFBFBD>P<EFBFBD>[<5B>~<7E>Ȥ䴩<C8A4>B,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z
,,<EFBFBD>~<7E>ȥͺ޳<CDBA>,<EFBFBD>g<EFBFBD>Ʋz
,,<EFBFBD>~<7E>ȥͺ޽<CDBA>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD><EFBFBD>y&<26><><EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>B<EFBFBD><EFBFBD>
,,,<EFBFBD>g<EFBFBD>z
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD><EFBFBD>P<EFBFBD><EFBFBD><EFBFBD>s,<EFBFBD>M<EFBFBD><EFBFBD>
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Բ<EFBFBD>,<EFBFBD>M<EFBFBD><EFBFBD>
,,MOSFET<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>Ҫ<EFBFBD>
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,<EFBFBD>j<EFBFBD><EFBFBD><EFBFBD>ذϾP<EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E>B,<EFBFBD>j<EFBFBD><EFBFBD><EFBFBD>ذϾP<EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E>B,<EFBFBD>B<EFBFBD><EFBFBD>
,,<EFBFBD>x<EFBFBD>W<EFBFBD>Ϸ~<7E>ȳ<EFBFBD>,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>U<EFBFBD>z
,,<EFBFBD>~<7E>Ȥ@<40><>,<EFBFBD>B<EFBFBD><EFBFBD>/<2F><><EFBFBD>`<60>g<EFBFBD>z
,,,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>U<EFBFBD>z
,,<EFBFBD>~<7E>ȤG<C8A4><47>,<EFBFBD>B<EFBFBD><EFBFBD>/<2F><><EFBFBD>`<60>g<EFBFBD>z
,,,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>U<EFBFBD>z
,,HH<EFBFBD>M<EFBFBD>ײ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
,,,<EFBFBD>M<EFBFBD><EFBFBD>
,,,<EFBFBD>U<EFBFBD>z
1 事業體 處別 單位名稱 崗位名稱
2 半導體事業群 半導體事業群 半導體事業群 營運長
3 營運長助理
4 汽車事業體 汽車事業體 汽車事業體 副總經理
5 專案經理
6 法務室 法務室 法務室 經副理
7 法務專員
8 專利工程師
9 岡山製造事業體 生產處 生產處 處長
10 專員
11 生產部 經副理
12 生產課 課長
13 組長
14 班長
15 副班長
16 作業員
17 生產企劃部 經副理
18 課長
19 專員
20 工程師
21 岡山製造事業體 岡山品質管制部 經副理
22 封裝品質管制課 課長
23 工程師
24 組長
25 班長
26 副班長
27 作業員
28 品質管制整合課 課長
29 工程師
30 岡山製造事業體 副總經理
31 副總經理助理
32 封裝工程處 封裝工程處 處長
33 專員
34 工程師
35 製程工程一部 經副理
36 製程工程二部 經副理
37 上片銲線工程課 課長
38 工程師
39 切割點膠工程課 課長
40 工程師
41 正印~彎腳工程課 課長
42 工程師
43 模壓電鍍工程課 課長
44 工程師
45 設備一部 經副理
46 設備二部 經副理
47 設備一課 課長
48 工程師
49 設備二課 課長
50 工程師
51 設備三課 課長
52 工程師
53 副總辦公室 工業工程部 經副理
54 工程師
55 工業工程課 課長
56 工程師
57 專案管理 副理
58 工程師
59 測試工程與研發處 測試工程與研發處 處長
60 專員
61 測試工程部 經副理
62 設備課 課長
63 工程師
64 測試課 課長
65 工程師
66 新產品導入部 經副理
67 專員
68 工程師
69 研發部 經副理
70 封裝技術課 課長
71 工程師
72 設計模擬課 課長
73 工程師
74 專員
75 資材處 資材處 處長
76 採購部 經副理
77 採購一課 課長
78 專員
79 採購二課 課長
80 專員
81 外部資源部 專員
82 生管部 經副理
83 生產排程課 課長
84 專員
85 成品倉 課長
86 班長
87 副班長
88 作業員
89 原物料控制部 經副理
90 物料控制課 課長
91 專員
92 原物料倉 班長
93 副班長
94 作業員
95 廠務與環安衛管理處 廠務與環安衛管理處 處長
96 工程師
97 廠務部 經副理
98 廠務課 課長
99 工程師
100 安衛中心 課長
101 工程師
102 專員
103 智動化課 課長
104 工程師
105 產品事業體 產品事業體 產品事業體 處長
106 先進產品事業處 先進產品事業處 處長
107 產品管理部(APD) 經副理
108 產品工程(APD) 經副理
109 工程師
110 產品管理(APD) 經副理
111 工程師
112 成熟產品事業處 成熟產品事業處 處長
113 產品管理部(MPD) 經副理
114 產品工程(MPD) 經副理
115 專案經副理
116 工程師
117 產品管理(MPD) 經副理
118 專案經副理
119 工程師
120 晶圓三廠 晶圓三廠 晶圓三廠 顧問
121 專員
122 品質部 經副理
123 工程師
124 作業員
125 製造部 經副理
126 課長
127 班長
128 副班長
129 作業員
130 廠務部(Fab3) 經副理
131 工程師
132 製程工程處 工程一部 經副理
133 工程師
134 工程二部 經副理
135 工程師
136 工程三部 經副理
137 工程師
138 製程整合部(Fab3) 經副理
139 工程師
140 集團人資行政事業體 集團人資行政事業體 集團人資行政事業體 人資長
141 行政總務管理部 經副理
142 專員
143 助理
144 招募任用部 經副理
145 專員
146 訓練發展部 經副理
147 專員
148 薪酬管理部 經副理
149 專員
150 集團財務事業體 集團財務事業體 集團財務事業體 財務長
151 岡山強茂財務處 岡山強茂財務處 處長
152 岡山強茂財務部 經副理
153 岡山強茂財務課 課長
154 專員
155 集團財務事業體 集團投資人關係 專案副理
156 集團會計事業體 集團會計事業體 集團會計事業體 會計長
157 岡山會計處 岡山會計處 處長
158 岡山會計處 會計部 經副理
159 岡山會計處 會計課 課長
160 岡山會計處 專員
161 岡山會計處 稅務課 課長
162 岡山會計處 專員
163 岡山會計處 管理會計部 經副理
164 岡山會計處 封裝管理會計課 課長
165 岡山會計處 專員
166 岡山會計處 晶圓管理會計課 課長
167 岡山會計處 專員
168 集團會計處 集團會計處 處長
169 集團會計處 集團合併報表部 經副理
170 集團會計處 專員
171 集團資訊事業體 集團資訊事業體 集團資訊事業體 資訊長
172 資安行動小組 資安行動課 課長
173 資訊一處 應用系統部 經副理
174 資訊一處 工程師
175 資訊一處 電腦整合製造部 經副理
176 資訊一處 工程師
177 資訊一處 系統網路服務部 經副理
178 資訊一處 工程師
179 資訊二處 資訊二處 處長
180 新創事業體 新創事業體 新創事業體 處長
181 新創事業體 資源管理部 經副理
182 新創事業體 專員
183 研發中心 中低壓產品研發處 經副理
184 研發中心 工程師
185 研發中心 高壓產品研發處 經副理
186 研發中心 工程師
187 稽核室 稽核室 稽核室 主任
188 專員
189 總經理室 總經理室 總經理室 總裁
190 總經理
191 ESG專案辦公室 ESG專案辦公室 經副理
192 環境永續小組 課長
193 專員/工程師
194 社會永續小組 課長
195 專員/工程師
196 公司治理永續小組 課長
197 專員/工程師
198 專案管理室 專案管理室 副總經理
199 專案管理 經副理
200 專員/工程師
201 PVS專案課 專員/工程師
202 總品質事業體 總品質事業體 總品質事業體 處長
203 客戶品質管理部 經副理
204 客戶品質工程課 課長
205 工程師
206 專員
207 品質工程課 課長
208 工程師
209 產品品質管理部 經副理
210 新產品品質管理課 課長
211 工程師
212 變更管理課 課長
213 工程師
214 客戶支援工程課 課長
215 工程師
216 班長
217 作業員
218 品質系統及客戶工程整合部 經副理
219 客戶工程整合課 課長
220 工程師
221 品質系統課 課長
222 工程師
223 封測外包品質管理部 經副理
224 課長
225 工程師
226 品質保證部 經副理
227 失效分析課 課長
228 工程師
229 供應商管理課 課長
230 工程師
231 班長
232 副班長
233 作業員
234 信賴性保證課 課長
235 工程師
236 班長
237 副班長
238 作業員
239 晶圓供應商管理課 課長
240 工程師
241 營業事業體 營業事業體 營業事業體 副總經理
242 副總經理助理
243 商業開發暨市場應用處 商業開發暨市場應用處 處長
244 經理
245 工程師
246 海外銷售事業處 海外銷售事業處 處長
247 日本區暨代工業務部 經副理
248 日本區 課長
249 專員
250 助理
251 代工 課長
252 專員
253 助理
254 歐亞區業務部 經副理
255 助理
256 歐洲 課長
257 專員
258 助理
259 南亞 課長
260 專員
261 助理
262 東協 課長
263 專員
264 助理
265 韓國區業務部-韓國區 經副理
266 課長
267 專員
268 助理
269 全球跨區客戶管理 專案經理
270 經副理
271 美洲區業務部 經副理
272 課長
273 專員
274 助理
275 全球技術服務處 全球技術服務處 處長
276 工程師
277 助理
278 應用工程部(GTS) 經副理
279 專案經副理
280 技術經副理
281 工程師
282 系統工程部 經副理
283 工程師
284 特性測試部 經副理
285 特性測試課 課長
286 工程師
287 全球行銷暨業務支援處 全球行銷暨業務支援處 副總經理
288 業務生管部 經副理
289 業務生管課 課長
290 專員
291 物流&船務 課長
292 專員
293 市場行銷企劃部 處長
294 經理
295 專員
296 行銷推廣 專員
297 市場戰略 專員
298 MOSFET晶圓採購部 經副理
299 課長
300 專員
301 大中華區銷售事業處 大中華區銷售事業處 處長
302 台灣區業務部 專員
303 助理
304 業務一部 處長/資深經理
305 經副理
306 專員
307 助理
308 業務二部 處長/資深經理
309 經副理
310 專員
311 助理
312 HH專案組 經副理
313 專員
314 助理