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

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

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

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

View File

@@ -35,7 +35,11 @@
"Bash(powershell -Command \"Get-Process python | Stop-Process -Force\")",
"Bash(python llm_config.py:*)",
"Bash(python:*)",
"Bash(mkdir:*)"
"Bash(mkdir:*)",
"Bash(del \"d:\\00001_Vibe_coding\\1204剛為\\hierarchy_test.json\")",
"Bash(dir:*)",
"Bash(echo:*)",
"Bash(find:*)"
],
"deny": [],
"ask": []

5
.cursor/worktrees.json Normal file
View File

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

View File

@@ -1,567 +1,5 @@
# 軟體設計文件 (SDD) - 代碼分離優化
> **文件版本**: v1.0
> **建立日期**: 2024-12-04
> **專案名稱**: HR Position Management System
> **優化目標**: 將 CSS 和 JavaScript 從 index.html 分離
> **狀態**: 📋 待決策階段
---
## 📋 目錄
1. [專案現況分析](#專案現況分析)
2. [優化目標與效益](#優化目標與效益)
3. [需要決策的事項](#需要決策的事項)
4. [建議的架構方案](#建議的架構方案)
5. [分離策略](#分離策略)
6. [風險評估](#風險評估)
7. [執行計畫](#執行計畫)
8. [驗收標準](#驗收標準)
---
## 專案現況分析
### 📊 當前檔案結構
```
d:\00001_Vibe_coding\1204剛為\
├── index.html (約 4,700 行) ⚠️ 包含 HTML + CSS + JavaScript
├── login.html (約 470 行) ⚠️ 包含 HTML + CSS + JavaScript
├── hierarchical_data.js (已分離) ✅ 組織架構資料
├── dropdown_data.js (已分離) ✅ 下拉選單資料
├── app.py (Flask 後端)
├── .env (環境設定)
└── prompt.md (AI Prompt 文件)
```
### 📈 當前 index.html 組成分析
| 內容類型 | 預估行數 | 佔比 | 說明 |
|---------|---------|------|------|
| **HTML 結構** | ~1,500 行 | 32% | 5 個主要頁籤的表單結構 |
| **CSS 樣式** | ~700 行 | 15% | 內嵌於 `<style>` 標籤中 |
| **JavaScript** | ~2,500 行 | 53% | 內嵌於 `<script>` 標籤中 |
### ⚠️ 當前問題
1. **維護困難**: 單一檔案過於龐大,難以快速定位問題
2. **協作不便**: 多人同時修改容易產生衝突
3. **載入效能**: 無法利用瀏覽器快取分離的資源檔案
4. **程式碼重用**: CSS 和 JS 無法在其他頁面(如 login.html重用
5. **偵錯困難**: 瀏覽器開發者工具中程式碼不易閱讀
6. **版本控制**: Git diff 難以追蹤具體修改內容
---
## 優化目標與效益
### 🎯 優化目標
1. **分離關注點**: HTML 結構、CSS 樣式、JavaScript 邏輯各自獨立
2. **提升可維護性**: 每個檔案職責單一,易於理解和修改
3. **改善效能**: 利用瀏覽器快取機制,減少重複載入
4. **便於協作**: 不同團隊成員可同時修改不同檔案
5. **增強可測試性**: JavaScript 獨立後更容易進行單元測試
### 📈 預期效益
| 效益項目 | 改善程度 | 說明 |
|---------|---------|------|
| **維護效率** | ⬆️ 40-60% | 快速定位和修改程式碼 |
| **載入速度** | ⬆️ 20-30% | 瀏覽器快取 CSS/JS 檔案 |
| **協作效率** | ⬆️ 50-70% | 減少 Git 衝突,平行開發 |
| **程式碼重用** | ⬆️ 80-100% | CSS/JS 可在多個頁面共用 |
| **偵錯效率** | ⬆️ 30-50% | 獨立檔案Source Map 支援 |
---
## 需要決策的事項
> 🔔 請針對以下問題做出選擇,我會根據您的決策提供對應的實作方案
---
### ❓ 決策 1: CSS 分離策略
**問題**: 如何分離 CSS 樣式?
#### 選項 A: 單一 CSS 檔案(簡單方案)✨ 推薦新手
```
styles/
└── main.css (包含所有樣式)
```
**優點**:
- ✅ 結構簡單,易於實作
- ✅ 只需引入一個 CSS 檔案
- ✅ 適合小型專案
**缺點**:
- ❌ 單一檔案可能過大
- ❌ 不同頁面的樣式混在一起
---
#### 選項 B: 模組化 CSS 檔案(推薦方案)✨ 推薦
```
styles/
├── base.css (基礎樣式、CSS Reset、變數)
├── layout.css (整體布局、Grid、Flexbox)
├── components.css (按鈕、表單、卡片等元件)
├── modules.css (5 個頁籤的專屬樣式)
└── utilities.css (工具類別、響應式)
```
**優點**:
- ✅ 職責分明,易於維護
- ✅ 可按需載入(未來優化)
- ✅ 符合業界最佳實踐
**缺點**:
- ❌ 需要管理多個檔案
- ❌ 初期設定較複雜
---
#### 選項 C: 按頁籤分離 CSS精細方案
```
styles/
├── common.css (共用樣式)
├── position-basic.css (崗位基礎資料)
├── position-recruit.css (崗位招聘要求)
├── job-basic.css (職務基礎資料)
├── dept-function.css (部門職責維護)
├── job-desc.css (崗位描述)
└── position-list.css (崗位清單)
```
**優點**:
- ✅ 極度模組化
- ✅ 按需載入效能最佳
- ✅ 適合大型專案
**缺點**:
- ❌ 檔案數量多,管理複雜
- ❌ 共用樣式可能重複定義
---
**您的選擇**: [ ] A / [ ] B / [ ] C / [ ] 其他建議: _______________
---
### ❓ 決策 2: JavaScript 分離策略
**問題**: 如何組織 JavaScript 程式碼?
#### 選項 A: 功能模組化(推薦方案)✨ 推薦
```
js/
├── config.js (設定檔API URL、常數)
├── utils.js (工具函式showToast、fillIfEmpty)
├── api.js (API 呼叫callClaudeAPI、fetch 相關)
├── ui.js (UI 操作switchModule、updatePreview)
├── form-handlers.js (表單處理:驗證、提交)
├── ai-generators.js (AI 生成函式5 個 generate 函式)
├── dropdown.js (下拉選單邏輯:階層式選單)
└── main.js (主程式:初始化、事件監聽)
```
**優點**:
- ✅ 職責清晰,易於維護
- ✅ 符合 MVC/MVVM 架構思想
- ✅ 便於單元測試
- ✅ 符合業界最佳實踐
**缺點**:
- ❌ 需要處理模組間依賴關係
- ❌ 檔案數量較多
---
#### 選項 B: 按頁籤分離(直觀方案)
```
js/
├── common.js (共用功能)
├── position-basic.js (崗位基礎資料)
├── position-recruit.js (崗位招聘要求)
├── job-basic.js (職務基礎資料)
├── dept-function.js (部門職責維護)
├── job-desc.js (崗位描述)
└── position-list.js (崗位清單)
```
**優點**:
- ✅ 對應頁籤結構,直觀易懂
- ✅ 修改特定頁籤功能時容易定位
**缺點**:
- ❌ 共用函式可能在多個檔案重複
- ❌ 跨頁籤功能需要協調多個檔案
---
#### 選項 C: 混合方案(平衡方案)✨ 折衷選擇
```
js/
├── core/
│ ├── config.js (設定)
│ ├── utils.js (工具)
│ ├── api.js (API)
│ └── ui.js (UI)
├── modules/
│ ├── position-basic.js
│ ├── position-recruit.js
│ ├── job-basic.js
│ ├── dept-function.js
│ ├── job-desc.js
│ └── position-list.js
└── main.js (主程式)
```
**優點**:
- ✅ 兼具模組化與直觀性
- ✅ 核心功能集中管理
- ✅ 頁籤功能獨立開發
**缺點**:
- ❌ 目錄結構較複雜
- ❌ 需要規劃模組間通訊
---
**您的選擇**: [ ] A / [ ] B / [ ] C / [ ] 其他建議: _______________
---
### ❓ 決策 3: 模組載入方式
**問題**: 如何在 HTML 中載入分離後的 JS 檔案?
#### 選項 A: 傳統 `<script>` 標籤(相容性最佳)✨ 推薦相容
```html
<script src="js/config.js"></script>
<script src="js/utils.js"></script>
<script src="js/api.js"></script>
<!-- 按順序載入,注意依賴關係 -->
```
**優點**:
- ✅ 相容所有瀏覽器(包含 IE11
- ✅ 不需要打包工具
- ✅ 實作簡單
**缺點**:
- ❌ 需要手動管理載入順序
- ❌ 全域命名空間污染風險
- ❌ 無法使用 `import/export`
---
#### 選項 B: ES6 Modules現代方案✨ 推薦現代
```html
<script type="module" src="js/main.js"></script>
<!-- main.js 中使用 import 載入其他模組 -->
```
```javascript
// main.js
import { showToast } from './utils.js';
import { callClaudeAPI } from './api.js';
```
**優點**:
- ✅ 現代瀏覽器原生支援
- ✅ 自動處理依賴關係
- ✅ 避免全域污染
- ✅ 支援按需載入Tree Shaking
**缺點**:
- ❌ 不支援 IE11需要 polyfill
- ❌ 本地開發需要 HTTP Server不能用 file:// 協議)
- ❌ 需要調整現有程式碼結構
---
#### 選項 C: 打包工具Webpack/Vite進階方案
```html
<script src="dist/bundle.js"></script>
<!-- 所有 JS 打包成單一檔案 -->
```
**優點**:
- ✅ 最佳效能(壓縮、優化)
- ✅ 支援所有瀏覽器
- ✅ 可使用 npm 套件
- ✅ 支援 TypeScript、Babel 等工具
**缺點**:
- ❌ 需要安裝 Node.js 和打包工具
- ❌ 開發流程變複雜(需要 build
- ❌ 學習曲線較陡
---
**您的選擇**: [ ] A / [ ] B / [ ] C / [ ] 其他建議: _______________
---
### ❓ 決策 4: 共用樣式處理
**問題**: index.html 和 login.html 有部分共用樣式,如何處理?
#### 選項 A: 建立共用樣式檔(推薦)✨ 推薦
```
styles/
├── common.css (兩個頁面共用的樣式)
├── index.css (index.html 專屬)
└── login.css (login.html 專屬)
```
**優點**:
- ✅ 避免重複程式碼
- ✅ 樣式一致性高
- ✅ 修改一處,全站生效
**缺點**:
- ❌ 需要識別哪些是共用樣式
---
#### 選項 B: 各自獨立(簡單方案)
```
styles/
├── index.css (包含所有 index.html 需要的樣式)
└── login.css (包含所有 login.html 需要的樣式)
```
**優點**:
- ✅ 實作最簡單
- ✅ 兩頁面完全獨立
**缺點**:
- ❌ 樣式重複(如按鈕、表單元件)
- ❌ 修改需要同步更新多處
---
**您的選擇**: [ ] A / [ ] B / [ ] 其他建議: _______________
---
### ❓ 決策 5: CSS 變數與主題管理
**問題**: 系統使用紫色漸層主題,是否要統一管理?
#### 選項 A: 使用 CSS 變數(推薦)✨ 推薦
```css
/* base.css */
:root {
/* 主題色 */
--primary-color: #667eea;
--primary-dark: #764ba2;
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
/* 語義化顏色 */
--color-success: #10b981;
--color-warning: #f59e0b;
--color-danger: #ef4444;
/* 間距 */
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
/* 字體 */
--font-family: 'Microsoft JhengHei', 'Segoe UI', sans-serif;
--font-size-base: 14px;
}
```
**優點**:
- ✅ 統一管理主題色
- ✅ 輕鬆實現換色/Dark Mode
- ✅ 語義化命名,易於理解
**缺點**:
- ❌ 不支援 IE11需要 polyfill
---
#### 選項 B: 直接寫死顏色值(簡單方案)
```css
.button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
```
**優點**:
- ✅ 相容所有瀏覽器
- ✅ 不需要額外設定
**缺點**:
- ❌ 修改主題色需要全局搜尋替換
- ❌ 無法動態切換主題
---
**您的選擇**: [ ] A / [ ] B / [ ] 其他建議: _______________
---
### ❓ 決策 6: 執行順序與回滾計畫
**問題**: 如何進行代碼分離,以降低風險?
#### 選項 A: 一次性全部分離(快速方案)
**執行步驟**:
1. 一次性建立所有 CSS/JS 檔案
2. 從 index.html 移除所有 `<style>``<script>`
3. 完整測試所有功能
**優點**:
- ✅ 快速完成重構
- ✅ 避免中間狀態
**缺點**:
- ❌ 風險較高,問題多時難以回滾
- ❌ 測試工作量大
---
#### 選項 B: 分階段漸進式分離(穩健方案)✨ 推薦
**階段 1**: CSS 分離1-2 天)
- 建立 CSS 檔案
- 測試樣式正確性
- Git commit
**階段 2**: JavaScript 工具函式分離1-2 天)
- 分離 utils.js、api.js
- 測試基礎功能
- Git commit
**階段 3**: JavaScript 模組分離2-3 天)
- 按選定策略分離各模組
- 逐一測試各頁籤功能
- Git commit
**階段 4**: 優化與收尾1 天)
- 程式碼審查
- 效能測試
- 文件更新
**優點**:
- ✅ 風險可控,問題易定位
- ✅ 每階段可獨立回滾
- ✅ 便於測試和驗證
**缺點**:
- ❌ 時間較長
- ❌ 可能存在中間狀態
---
#### 選項 C: 建立平行版本(保守方案)
**執行步驟**:
1. 複製 index.html 為 index-refactor.html
2. 在新檔案中進行重構
3. 完成後替換原檔案
**優點**:
- ✅ 最安全,隨時可回滾
- ✅ 可對比新舊版本
**缺點**:
- ❌ 需要維護兩個版本
- ❌ 浪費開發資源
---
**您的選擇**: [ ] A / [ ] B / [ ] C / [ ] 其他建議: _______________
---
### ❓ 決策 7: 命名規範
**問題**: 檔案和函式的命名風格?
#### 選項 A: Kebab-case檔案+ camelCase函式✨ 推薦
```
檔案名稱: form-handlers.js, ai-generators.js
函式名稱: generatePositionBasic(), showToast()
```
**優點**:
- ✅ 符合業界慣例
- ✅ URL 友善(不需要編碼)
---
#### 選項 B: camelCase檔案+ camelCase函式
```
檔案名稱: formHandlers.js, aiGenerators.js
函式名稱: generatePositionBasic(), showToast()
```
**優點**:
- ✅ 與 JavaScript 慣例一致
**缺點**:
- ❌ Windows/Mac 檔案系統可能有大小寫問題
---
#### 選項 C: snake_case檔案+ camelCase函式
```
檔案名稱: form_handlers.js, ai_generators.js
函式名稱: generatePositionBasic(), showToast()
```
**優點**:
- ✅ Python 開發者熟悉
**缺點**:
- ❌ JavaScript 社群較少使用
---
**您的選擇**: [ ] A / [ ] B / [ ] C / [ ] 其他建議: _______________
---
### ❓ 決策 8: 瀏覽器相容性
**問題**: 系統需要支援哪些瀏覽器?
#### 選項 A: 現代瀏覽器Chrome/Firefox/Safari/Edge 最新版)✨ 推薦
**技術選擇**:
- ✅ 可使用 ES6 Modules
- ✅ 可使用 CSS Grid、Flexbox
- ✅ 可使用 CSS Variables
- ✅ 開發效率高
**支援範圍**:
- Chrome 60+
- Firefox 60+
- Safari 11+
- Edge 79+ (Chromium)
---
#### 選項 B: 包含 IE11企業環境
**技術選擇**:
- ❌ 需要使用 Babel 轉譯
- ❌ 需要 CSS Variables polyfill
- ❌ 需要 Flexbox fallback
- ❌ 開發成本增加 30-50%
---
**您的選擇**: [ ] A / [ ] B / [ ] 其他說明: _______________
---
## 建議的架構方案
### 🏆 我的推薦配置(根據專案特性)
基於您的專案現況,我推薦以下配置:
```
✅ 決策 1: 選項 B - 模組化 CSS 檔案
✅ 決策 2: 選項 A - 功能模組化 JavaScript
✅ 決策 3: 選項 B - ES6 Modules需要 HTTP Server
✅ 決策 4: 選項 A - 建立共用樣式檔
✅ 決策 5: 選項 A - 使用 CSS 變數
✅ 決策 6: 選項 B - 分階段漸進式分離
✅ 決策 7: 選項 A - Kebab-case + camelCase
✅ 決策 8: 選項 A - 現代瀏覽器
```
### 📁 推薦的最終檔案結構

View File

@@ -1,485 +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本文件
- ✅ 更新 .gitignore排除 USER_COMMANDS_LOG.md
- ✅ 成功推送至 Giteacommit: b258477
---
## 📊 指令統計
**總計**: 18 個指令
**已完成**: 18 個 ✅
**進行中**: 0 個
---
## 🔧 技術決策記錄
### 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
**專案狀態**: 🚧 開發中

329
app.py
View File

@@ -27,12 +27,23 @@ except ImportError:
print("Warning: llm_config not found. LLM features will be disabled.")
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.config['JSON_AS_ASCII'] = False # 確保 JSON 正確處理中文
CORS(app)
# 模擬資料庫 (實際應用中應使用 MySQL/PostgreSQL)
positions_db = {}
HR_position_positions_db = {}
# 預設崗位資料
default_positions = {
@@ -76,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 = {
@@ -102,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 = {
@@ -123,7 +134,7 @@ default_descriptions = {
}
}
position_descriptions_db.update(default_descriptions)
HR_position_descriptions_db.update(default_descriptions)
# ==================== 靜態頁面 ====================
@@ -267,7 +278,7 @@ def import_positions_csv():
continue
# 檢查是否已存在
if position_code in positions_db:
if position_code in HR_position_positions_db:
errors.append(f"{row_num} 列: 崗位編號 {position_code} 已存在")
error_count += 1
continue
@@ -313,7 +324,7 @@ def import_positions_csv():
'updatedAt': now
}
positions_db[position_code] = new_position
HR_position_positions_db[position_code] = new_position
success_count += 1
except Exception as e:
@@ -349,7 +360,7 @@ def get_positions():
search = request.args.get('search', '', type=str)
# 過濾搜尋
filtered = list(positions_db.values())
filtered = list(HR_position_positions_db.values())
if search:
filtered = [
p for p in filtered
@@ -378,7 +389,7 @@ def get_positions():
@app.route('/api/positions/<position_id>', methods=['GET'])
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': '找不到該崗位資料'
@@ -386,7 +397,7 @@ def get_position(position_id):
return jsonify({
'success': True,
'data': positions_db[position_id]
'data': HR_position_positions_db[position_id]
})
@@ -425,7 +436,7 @@ def create_position():
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} 已存在'
@@ -441,7 +452,7 @@ def create_position():
'updatedAt': now
}
positions_db[position_code] = new_position
HR_position_positions_db[position_code] = new_position
return jsonify({
'success': True,
@@ -465,7 +476,7 @@ def update_position(position_id):
}
"""
try:
if position_id not in positions_db:
if position_id not in HR_position_positions_db:
return jsonify({
'success': False,
'error': '找不到該崗位資料'
@@ -479,7 +490,7 @@ def update_position(position_id):
}), 400
# 更新資料
existing = positions_db[position_id]
existing = HR_position_positions_db[position_id]
if 'basicInfo' in data:
existing['basicInfo'].update(data['basicInfo'])
@@ -505,13 +516,13 @@ def update_position(position_id):
def delete_position(position_id):
"""刪除崗位資料"""
try:
if position_id not in positions_db:
if position_id not in HR_position_positions_db:
return jsonify({
'success': False,
'error': '找不到該崗位資料'
}), 404
deleted = positions_db.pop(position_id)
deleted = HR_position_positions_db.pop(position_id)
return jsonify({
'success': True,
@@ -532,7 +543,7 @@ def change_position_code(position_id):
Request body: { newCode: "新編號" }
"""
try:
if position_id not in positions_db:
if position_id not in HR_position_positions_db:
return jsonify({
'success': False,
'error': '找不到該崗位資料'
@@ -547,19 +558,19 @@ def change_position_code(position_id):
'error': '請提供新的崗位編號'
}), 400
if new_code in positions_db:
if new_code in HR_position_positions_db:
return jsonify({
'success': False,
'error': f'崗位編號 {new_code} 已存在'
}), 409
# 更新編號
position = positions_db.pop(position_id)
position = HR_position_positions_db.pop(position_id)
position['id'] = new_code
position['basicInfo']['positionCode'] = new_code
position['updatedAt'] = datetime.now().isoformat()
positions_db[new_code] = position
HR_position_positions_db[new_code] = position
return jsonify({
'success': True,
@@ -662,7 +673,7 @@ def import_jobs_csv():
continue
# 檢查是否已存在
if job_code in jobs_db:
if job_code in HR_position_jobs_db:
errors.append(f"{row_num} 列: 職務編號 {job_code} 已存在")
error_count += 1
continue
@@ -687,7 +698,7 @@ def import_jobs_csv():
'updatedAt': now
}
jobs_db[job_code] = new_job
HR_position_jobs_db[job_code] = new_job
success_count += 1
except Exception as e:
@@ -725,7 +736,7 @@ def get_jobs():
category = request.args.get('category', '', type=str)
# 過濾搜尋
filtered = list(jobs_db.values())
filtered = list(HR_position_jobs_db.values())
if search:
filtered = [
j for j in filtered
@@ -759,7 +770,7 @@ def get_jobs():
@app.route('/api/jobs/<job_id>', methods=['GET'])
def get_job(job_id):
"""獲取單一職務資料"""
if job_id not in jobs_db:
if job_id not in HR_position_jobs_db:
return jsonify({
'success': False,
'error': '找不到該職務資料'
@@ -767,7 +778,7 @@ def get_job(job_id):
return jsonify({
'success': True,
'data': jobs_db[job_id]
'data': HR_position_jobs_db[job_id]
})
@@ -814,7 +825,7 @@ def create_job():
job_code = data['jobCode']
# 檢查是否已存在
if job_code in jobs_db:
if job_code in HR_position_jobs_db:
return jsonify({
'success': False,
'error': f'職務編號 {job_code} 已存在'
@@ -829,7 +840,7 @@ def create_job():
'updatedAt': now
}
jobs_db[job_code] = new_job
HR_position_jobs_db[job_code] = new_job
return jsonify({
'success': True,
@@ -847,7 +858,7 @@ def create_job():
def update_job(job_id):
"""更新職務資料"""
try:
if job_id not in jobs_db:
if job_id not in HR_position_jobs_db:
return jsonify({
'success': False,
'error': '找不到該職務資料'
@@ -861,7 +872,7 @@ def update_job(job_id):
}), 400
# 更新資料
existing = jobs_db[job_id]
existing = HR_position_jobs_db[job_id]
existing.update(data)
existing['updatedAt'] = datetime.now().isoformat()
@@ -881,13 +892,13 @@ def update_job(job_id):
def delete_job(job_id):
"""刪除職務資料"""
try:
if job_id not in jobs_db:
if job_id not in HR_position_jobs_db:
return jsonify({
'success': False,
'error': '找不到該職務資料'
}), 404
deleted = jobs_db.pop(job_id)
deleted = HR_position_jobs_db.pop(job_id)
return jsonify({
'success': True,
@@ -908,7 +919,7 @@ def change_job_code(job_id):
Request body: { newCode: "新編號" }
"""
try:
if job_id not in jobs_db:
if job_id not in HR_position_jobs_db:
return jsonify({
'success': False,
'error': '找不到該職務資料'
@@ -923,19 +934,19 @@ def change_job_code(job_id):
'error': '請提供新的職務編號'
}), 400
if new_code in jobs_db:
if new_code in HR_position_jobs_db:
return jsonify({
'success': False,
'error': f'職務編號 {new_code} 已存在'
}), 409
# 更新編號
job = jobs_db.pop(job_id)
job = HR_position_jobs_db.pop(job_id)
job['id'] = new_code
job['jobCode'] = new_code
job['updatedAt'] = datetime.now().isoformat()
jobs_db[new_code] = job
HR_position_jobs_db[new_code] = job
return jsonify({
'success': True,
@@ -1040,14 +1051,14 @@ def get_position_descriptions():
"""獲取所有崗位描述"""
return jsonify({
'success': True,
'data': list(position_descriptions_db.values())
'data': list(HR_position_descriptions_db.values())
})
@app.route('/api/position-descriptions/<position_code>', methods=['GET'])
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({
'success': False,
'error': '找不到該崗位描述'
@@ -1055,7 +1066,7 @@ def get_position_description(position_code):
return jsonify({
'success': True,
'data': position_descriptions_db[position_code]
'data': HR_position_descriptions_db[position_code]
})
@@ -1087,7 +1098,7 @@ def create_position_description():
}), 400
# 檢查崗位是否存在(暫時註解,允許直接新增描述)
# if position_code not in positions_db:
# if position_code not in HR_position_positions_db:
# return jsonify({
# 'success': False,
# 'error': f'崗位編號 {position_code} 不存在,請先建立崗位基礎資料'
@@ -1096,14 +1107,14 @@ def create_position_description():
now = datetime.now().isoformat()
# 如果已存在則更新,否則新增
if position_code in position_descriptions_db:
position_descriptions_db[position_code].update({
if position_code in HR_position_descriptions_db:
HR_position_descriptions_db[position_code].update({
**data,
'updatedAt': now
})
message = '崗位描述更新成功'
else:
position_descriptions_db[position_code] = {
HR_position_descriptions_db[position_code] = {
'id': position_code,
**data,
'createdAt': now,
@@ -1114,7 +1125,7 @@ def create_position_description():
return jsonify({
'success': True,
'message': message,
'data': position_descriptions_db[position_code]
'data': HR_position_descriptions_db[position_code]
})
except Exception as e:
@@ -1128,7 +1139,7 @@ def create_position_description():
def update_position_description(position_code):
"""更新崗位描述"""
try:
if position_code not in position_descriptions_db:
if position_code not in HR_position_descriptions_db:
return jsonify({
'success': False,
'error': '找不到該崗位描述'
@@ -1141,7 +1152,7 @@ def update_position_description(position_code):
'error': '請提供有效的 JSON 資料'
}), 400
position_descriptions_db[position_code].update({
HR_position_descriptions_db[position_code].update({
**data,
'updatedAt': datetime.now().isoformat()
})
@@ -1149,7 +1160,7 @@ def update_position_description(position_code):
return jsonify({
'success': True,
'message': '崗位描述更新成功',
'data': position_descriptions_db[position_code]
'data': HR_position_descriptions_db[position_code]
})
except Exception as e:
@@ -1163,13 +1174,13 @@ def update_position_description(position_code):
def delete_position_description(position_code):
"""刪除崗位描述"""
try:
if position_code not in position_descriptions_db:
if position_code not in HR_position_descriptions_db:
return jsonify({
'success': False,
'error': '找不到該崗位描述'
}), 404
deleted = position_descriptions_db.pop(position_code)
deleted = HR_position_descriptions_db.pop(position_code)
return jsonify({
'success': True,
@@ -1201,8 +1212,8 @@ def get_position_list():
# 組合崗位資料和描述
combined_list = []
for position_code, position in positions_db.items():
description = position_descriptions_db.get(position_code, {})
for position_code, position in HR_position_positions_db.items():
description = HR_position_descriptions_db.get(position_code, {})
combined = {
'positionCode': position_code,
@@ -1214,7 +1225,7 @@ def get_position_list():
'effectiveDate': position['basicInfo'].get('effectiveDate', ''),
'minEducation': position['recruitInfo'].get('minEducation', ''),
'salaryRange': position['recruitInfo'].get('salaryRange', ''),
'hasDescription': position_code in position_descriptions_db,
'hasDescription': position_code in HR_position_descriptions_db,
'jobDuties': description.get('jobDuties', ''),
'requiredSkills': description.get('requiredSkills', ''),
'workEnvironment': description.get('workEnvironment', ''),
@@ -1262,8 +1273,8 @@ def export_position_list():
# 組合所有崗位資料
rows = []
for position_code, position in positions_db.items():
description = position_descriptions_db.get(position_code, {})
for position_code, position in HR_position_positions_db.items():
description = HR_position_descriptions_db.get(position_code, {})
row = [
position_code,
@@ -1477,6 +1488,198 @@ def generate_llm_text():
}), 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)
@@ -1529,6 +1732,15 @@ if __name__ == '__main__':
║ GET /api/llm/test-all - 測試所有 API ║
║ 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 停止伺服器 ║
╚══════════════════════════════════════════════════════════════╝
""")
@@ -1543,5 +1755,14 @@ if __name__ == '__main__':
else:
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()
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;

View File

@@ -10,7 +10,7 @@ USE hr_position_system;
-- ============================================================
-- Table: positions (崗位基礎資料)
-- ============================================================
CREATE TABLE IF NOT EXISTS positions (
CREATE TABLE IF NOT EXISTS HR_position_positions (
id VARCHAR(20) PRIMARY KEY COMMENT '崗位編號',
position_code VARCHAR(20) NOT NULL UNIQUE COMMENT '崗位編號',
position_name VARCHAR(100) NOT NULL COMMENT '崗位名稱',
@@ -59,7 +59,7 @@ CREATE TABLE IF NOT EXISTS positions (
-- ============================================================
-- Table: jobs (職務基礎資料)
-- ============================================================
CREATE TABLE IF NOT EXISTS jobs (
CREATE TABLE IF NOT EXISTS HR_position_jobs (
id VARCHAR(20) PRIMARY KEY COMMENT '職務編號',
job_category_code VARCHAR(4) NOT NULL COMMENT '職務類別編號',
job_category_name VARCHAR(50) COMMENT '職務類別名稱',
@@ -88,7 +88,7 @@ CREATE TABLE IF NOT EXISTS jobs (
-- ============================================================
-- 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 '主鍵',
emp_no VARCHAR(20) 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_position_code (position_code),
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='崗位描述表';
-- ============================================================
-- 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 '主鍵',
code_type VARCHAR(50) NOT NULL COMMENT '代碼類型',
code_value VARCHAR(10) NOT NULL COMMENT '代碼值',
@@ -152,7 +152,7 @@ CREATE TABLE IF NOT EXISTS reference_codes (
-- ============================================================
-- 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 '主鍵',
action VARCHAR(20) NOT NULL COMMENT '操作類型(CREATE/UPDATE/DELETE)',
entity_type VARCHAR(50) NOT NULL COMMENT '實體類型',
@@ -174,21 +174,21 @@ CREATE TABLE IF NOT EXISTS audit_logs (
-- ============================================================
-- 崗位類別 (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', '02', '管理職', 2),
('POSITION_CATEGORY', '03', '業務職', 3),
('POSITION_CATEGORY', '04', '行政職', 4);
-- 崗位性質 (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', 'PT', '兼職', 'Part-time', 2),
('POSITION_NATURE', 'CT', '約聘', 'Contract', 3),
('POSITION_NATURE', 'IN', '實習', 'Intern', 4);
-- 崗位級別 (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', 'L2', '資深員工', 2),
('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);
-- 職務類別 (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', 'TECH', '技術職', 2),
('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);
-- 學歷 (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', 'JC', '專科', 'Junior College', 2),
('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);
-- 薪酬范圍 (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', 'B', 'B級', 2),
('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);
-- 任職地點 (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', 'TPE', '台北辦公室', 2),
('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);
-- 員工屬性 (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', 'CT', '約聘人員', 2),
('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);
-- 招聘職位 (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', 'MGR', '經理', 2),
('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);
-- 職稱要求 (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', 'CERT', '需證書', 2),
('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
-- ============================================================

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

@@ -1,8 +1,8 @@
# HR 基礎資料維護系統 - 軟體設計文件 (SDD)
**文件版本**2.1
**文件版本**3.1
**建立日期**2024-12-03
**最後更新**2024-12-04
**最後更新**2024-12-08
**文件狀態**Released
---
@@ -15,15 +15,17 @@
### 1.2 範圍
本系統涵蓋以下三大功能模組:
本系統涵蓋以下功能模組:
| 模組 | 功能說明 |
|------|----------|
| 崗位基礎資料 | 崗位主檔維護,含基礎資料與招聘要求 |
| 職務基礎資料 | 職務類別與屬性設定維護 |
| 部門職責 | 部門職責定義、使命願景與 KPI 維護 |
| 崗位描述 | 職責描述、崗位要求與任職條件維護 |
| 崗位清單 | 顯示所有崗位資料,支援查看描述與匯出 |
| 管理者頁面 | 使用者管理與完整崗位資料匯出 |
| 組織階層管理 | 事業體、處級單位、部級單位、崗位的層級關聯管理 |
### 1.3 參考文件
@@ -37,8 +39,14 @@
|------|------|
| 崗位 (Position) | 組織架構中的職位單位,具有編號、級別、編制人數等屬性 |
| 職務 (Job Title) | 職務類別分類,如管理職、技術職、業務職等 |
| 部門職責 (DeptFunction) | 部門的職責範圍、使命願景與績效指標定義 |
| 崗位描述 (Job Description) | 詳細描述崗位職責、要求與任職條件的文件 |
| 事業體 (Business Unit) | 組織最高層級,代表獨立經營單位 |
| 處級單位 (Division) | 事業體下的次級組織單位 |
| 部級單位 (Department) | 處級單位下的基層組織單位 |
| AI 自動填充 | 利用大型語言模型自動生成表單欄位內容的功能 |
| 三個錦囊 | AI 智能功能區塊,提供自動補齊、範例模板、驗證檢查 |
| LLM | Large Language Model大型語言模型 |
---
@@ -47,65 +55,117 @@
### 2.1 整體架構圖
```
┌─────────────────────────────────────────────────────────────────┐
┌─────────────────────────────────────────────────────────────────────
│ 使用者介面層 (UI Layer) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ │ 崗位基礎資料 │ │ 職務基礎資料 │ │ 崗位描述
│ │ 模組 │ 模組 模組 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘
│ ┌──────────────────────────────────────────────────────────┐
│ │ AI 自動填充服務 (Claude API)
└──────────────────────────────────────────────────────────
└─────────────────────────────────────────────────────────────────┘
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 崗位基礎 │ 職務基礎 │ │ 部門職責 崗位描述
│ │ 模組 │ 模組 │ 模組 模組 │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 三個錦囊 AI 功能區 │ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ │ 自動補齊 │ │ 範例模板 │ │ 驗證檢查 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ ES6 JavaScript 模組化架構 │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ main.js│ │ ui.js │ │ api.js │ │ai-bags │ │utils.js│ │ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
┌─────────────────────────────────────────────────────────────────────
│ 應用服務層 (Application Layer) │
│ ┌─────────────────────────────────────────────────────────┐
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Flask RESTful API │ │
│ │ ┌───────────┐ ┌────────── ┌───────────┐
│ │ │ Position │ Job │ │ Reference │ │
│ │ │ API │ API │ API │
│ │ └───────────┘ └────────── └───────────┘ │
│ └─────────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────────────┘
│ │ ┌────────── ┌──────────┐ ──────────┐ ┌──────────┐ │ │
│ │ │Position │ Job │ │DeptFunc │ │ LLM │ │Hierarchy│ │
│ │ │ API │ API │ API │ API │ │ API │ │
│ │ └────────── └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ LLM 配置模組 (llm_config.py) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Gemini │ │DeepSeek │ │ OpenAI │ │ Ollama │ │ GPT-OSS │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
┌─────────────────────────────────────────────────────────────────────
│ 資料存取層 (Data Layer) │
│ ┌─────────────────────────────────────────────────────────┐
│ │ In-Memory Database / Future DB
│ │ (positions_db, jobs_db, job_desc_db) │
└─────────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────────────┘
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ In-Memory Database / MySQL │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────────┐ │
│ │ positions_db │ │ 組織階層資料 │ │
│ │ │ jobs_db │ │ (business_units, divisions, │ │ │
│ │ │ dept_func_db │ │ departments, org_positions) │ │ │
│ │ │ job_desc_db │ │ │ │ │
│ │ └─────────────────┘ └─────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
### 2.2 技術堆疊
| 層級 | 技術選型 | 說明 |
|------|----------|------|
| 前端 | HTML5 + CSS3 + JavaScript | 純前端實作,無框架依賴 |
| 樣式 | Custom CSS + Google Fonts | Noto Sans TC 字型、CSS Variables |
| 前端 | HTML5 + CSS3 + ES6 JavaScript | 模組化架構,使用 import/export |
| 樣式 | CSS Modules + Google Fonts | Noto Sans TC 字型、CSS Variables |
| 後端 | Python Flask | RESTful API 服務 |
| AI 服務 | Claude API (Anthropic) | 智能表單填充 |
| 資料庫 | In-Memory (Dict) | 可擴展至 MySQL/PostgreSQL |
| AI 服務 | 多 LLM 支援 | Ollama、Gemini、DeepSeek、OpenAI、GPT-OSS |
| 資料庫 | 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
│ localhost:5000 │ │ api.anthropic
───────────────────┘ └───────────────────┘
┌─────────────────────────┐ ┌────────────────────────────────────┐
│ Flask Server │ │ LLM API Services
│ 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.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) 維護功能,包含崗位基本信息、職責描述與崗位要求三大區塊。
#### 3.3.2 頂部區域欄位
#### 3.4.2 頂部區域欄位
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|----------|--------|----------|------|
@@ -242,7 +340,7 @@
| 崗位代碼 | jd_positionCode | String(20) | |
| 版本更新日期 | jd_versionDate | Date | |
#### 3.3.3 崗位基本信息區塊
#### 3.4.3 崗位基本信息區塊
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|----------|--------|----------|------|
@@ -256,14 +354,14 @@
| 任職地點 | jd_workLocation | Enum | HQ/TPE/TYC/KHH/SH/SZ |
| 員工屬性 | jd_empAttribute | Enum | FT/CT/PT/IN/DP |
#### 3.3.4 職責描述區塊
#### 3.4.4 職責描述區塊
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|----------|--------|----------|------|
| 崗位設置目的 | jd_positionPurpose | String(500) | 單行文字 |
| 主要崗位職責 | jd_mainResponsibilities | Text | 編號格式 1、2、3、... |
#### 3.3.5 崗位要求區塊
#### 3.4.5 崗位要求區塊
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|----------|--------|----------|------|
@@ -273,7 +371,7 @@
| 工作經驗 | jd_workExperienceReq | Text | |
| 其他 | jd_otherRequirements | Text | |
#### 3.3.6 任職地點代碼對照表
#### 3.4.6 任職地點代碼對照表
| 代碼 | 名稱 |
|------|------|
@@ -284,7 +382,7 @@
| SH | 上海辦公室 |
| SZ | 深圳辦公室 |
#### 3.3.7 員工屬性代碼對照表
#### 3.4.7 員工屬性代碼對照表
| 代碼 | 名稱 |
|------|------|
@@ -358,6 +456,18 @@
| GET | `/api/reference/education` | 學歷選項 |
| 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.1 新增崗位
@@ -448,13 +558,51 @@ GET /api/positions?page=1&size=20&search=工程師
---
## 5. AI 自動填充功能設計
## 5. AI 智能功能設計
### 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
/**
@@ -510,7 +658,7 @@ function fillIfEmpty(elementId, value) {
}
```
#### 5.3.2 getEmptyFields
#### 5.6.2 getEmptyFields
```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**
```json
{
"model": "claude-sonnet-4-20250514",
"max_tokens": 2000,
"messages": [
{
"role": "user",
"content": "Prompt 內容..."
}
]
"api": "ollama",
"model": "qwen2.5:3b",
"prompt": "Prompt 內容...",
"max_tokens": 2000
}
```
### 5.5 Prompt 設計範例
### 5.8 Prompt 設計範例
```
請為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. 快捷鍵設計
@@ -758,9 +1001,35 @@ interface PositionListItem {
hr_position_form/
├── index.html # 前端應用主檔
├── 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 依賴套件
├── .env # 環境變數配置 (API Keys)
├── 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 功能 |
| 2.0 | 2024-12-04 | System | 新增 MySQL 資料庫整合、多 LLM API 支援、全局錯誤處理、Gitea 版本控制 |
| 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 解析錯誤處理測試(支援推理模型) |
---
**文件結束**

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/

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專案辦公室",
"專案管理室"
],
"總品質事業體": [
"總品質事業體"
],
"營業事業體": [
"營業事業體",
"商業開發暨市場應用處",
"海外銷售事業處",
"全球技術服務處",
"全球行銷暨業務支援處",
"大中華區銷售事業處"
]
}
}

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

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

3484
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;
}

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);
}
}

View File

@@ -59,8 +59,14 @@ export async function callClaudeAPI(prompt, api = 'ollama') {
// 清理 JSON 代碼塊標記和其他格式
let responseText = data.text;
// 移除 DeepSeek-R1 等模型的思考標籤 <think>...</think>
responseText = responseText.replace(/<think>[\s\S]*?<\/think>/gi, '');
// 移除 Markdown 代碼塊標記
responseText = responseText.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
responseText = responseText.replace(/```json\n?/gi, '').replace(/```\n?/g, '').trim();
// 移除可能的前導文字(如 "Here is the JSON:" 等)
responseText = responseText.replace(/^[\s\S]*?(?=\{)/i, '');
// 嘗試提取 JSON 內容(如果包含其他文字)
// 查找第一個 { 和最後一個 }
@@ -77,7 +83,18 @@ export async function callClaudeAPI(prompt, api = 'ollama') {
} catch (parseError) {
// JSON 解析失敗,拋出更詳細的錯誤
console.error('JSON 解析失敗,原始響應:', responseText);
throw new Error(`LLM 返回的內容不是有效的 JSON 格式。\n\n原始響應前 200 字符:\n${responseText.substring(0, 200)}...`);
// 嘗試修復常見的 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);

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;
}

View File

@@ -89,26 +89,26 @@ function setupFormListeners() {
field.addEventListener('input', updatePreview);
});
// 崗位類別變更
const positionCategory = document.getElementById('positionCategory');
// 崗位類別變更 (pos_category)
const positionCategory = document.getElementById('pos_category');
if (positionCategory) {
positionCategory.addEventListener('change', updateCategoryName);
}
// 崗位性質變更
const positionNature = document.getElementById('positionNature');
// 崗位性質變更 (pos_type)
const positionNature = document.getElementById('pos_type');
if (positionNature) {
positionNature.addEventListener('change', updateNatureName);
}
// 職務類別變更
const jobCategoryCode = document.getElementById('jobCategoryCode');
// 職務類別變更 (job_category)
const jobCategoryCode = document.getElementById('job_category');
if (jobCategoryCode) {
jobCategoryCode.addEventListener('change', updateJobCategoryName);
}
// Toggle 開關變更
const hasAttendanceBonus = document.getElementById('hasAttendanceBonus');
// Toggle 開關變更 (job_hasAttBonus)
const hasAttendanceBonus = document.getElementById('job_hasAttBonus');
if (hasAttendanceBonus) {
hasAttendanceBonus.addEventListener('change', function() {
const label = document.getElementById('attendanceLabel');
@@ -119,7 +119,8 @@ function setupFormListeners() {
});
}
const hasHousingAllowance = document.getElementById('hasHousingAllowance');
// Toggle 開關變更 (job_hasHouseAllow)
const hasHousingAllowance = document.getElementById('job_hasHouseAllow');
if (hasHousingAllowance) {
hasHousingAllowance.addEventListener('change', function() {
const label = document.getElementById('housingLabel');

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 格式,不要有任何其他文字。`
};

212
js/ui.js
View File

@@ -49,22 +49,51 @@ export function getPositionFormData() {
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'];
// 使用新的 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'
};
basicFields.forEach(field => {
const value = formData.get(field);
if (value) data.basicInfo[field] = value;
// 使用新的 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;
});
recruitFields.forEach(field => {
const value = formData.get(field);
if (value) data.recruitInfo[field] = value;
Object.entries(recruitFieldMapping).forEach(([htmlId, dataKey]) => {
const el = document.getElementById(htmlId);
if (el && el.value) data.recruitInfo[dataKey] = el.value;
});
return data;
@@ -76,19 +105,31 @@ export function getPositionFormData() {
*/
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'];
// 使用新的 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'
};
fields.forEach(field => {
const value = formData.get(field);
if (value) data[field] = value;
Object.entries(fieldMapping).forEach(([htmlId, dataKey]) => {
const el = document.getElementById(htmlId);
if (el && el.value) data[dataKey] = el.value;
});
data.hasAttendanceBonus = document.getElementById('hasAttendanceBonus').checked;
data.hasHousingAllowance = document.getElementById('hasHousingAllowance').checked;
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;
}
@@ -101,33 +142,59 @@ export function getJobDescFormData() {
const form = document.getElementById('jobDescForm');
if (!form) return {};
const formData = new FormData(form);
const data = { basicInfo: {}, positionInfo: {}, responsibilities: {}, requirements: {} };
// Basic Info
['empNo', 'empName', 'positionCode', 'versionDate'].forEach(field => {
const el = document.getElementById('jd_' + field);
if (el && el.value) data.basicInfo[field] = el.value;
// 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
['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;
// 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
const purpose = document.getElementById('jd_positionPurpose');
// Purpose & Responsibilities - 使用新的 jd_ prefix
const purpose = document.getElementById('jd_purpose');
if (purpose && purpose.value) data.responsibilities.positionPurpose = purpose.value;
const mainResp = document.getElementById('jd_mainResponsibilities');
const mainResp = document.getElementById('jd_mainResp');
if (mainResp && mainResp.value) data.responsibilities.mainResponsibilities = mainResp.value;
// Requirements
['education', 'basicSkills', 'professionalKnowledge', 'workExperienceReq', 'otherRequirements'].forEach(field => {
const el = document.getElementById('jd_' + field);
if (el && el.value) data.requirements[field] = el.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;
@@ -141,16 +208,33 @@ export function getDeptFunctionFormData() {
const form = document.getElementById('deptFunctionForm');
if (!form) return {};
const formData = new FormData(form);
const data = {};
const fields = ['deptFunctionCode', 'deptFunctionName', 'deptFunctionBU',
'deptFunctionDept', 'deptManager', 'deptMission', 'deptVision',
'deptCoreFunctions', 'deptKPIs'];
// 使用新的 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'
};
fields.forEach(field => {
const value = formData.get(field);
if (value) data[field] = value;
Object.entries(fieldMapping).forEach(([htmlId, dataKey]) => {
const el = document.getElementById(htmlId);
if (el && el.value) data[dataKey] = el.value;
});
return data;
@@ -192,8 +276,11 @@ export function updatePreview() {
* 更新崗位類別中文名稱
*/
export function updateCategoryName() {
const category = document.getElementById('positionCategory').value;
document.getElementById('positionCategoryName').value = categoryMap[category] || '';
const categoryEl = document.getElementById('pos_category');
const categoryNameEl = document.getElementById('pos_categoryName');
if (categoryEl && categoryNameEl) {
categoryNameEl.value = categoryMap[categoryEl.value] || '';
}
updatePreview();
}
@@ -201,8 +288,11 @@ export function updateCategoryName() {
* 更新崗位性質中文名稱
*/
export function updateNatureName() {
const nature = document.getElementById('positionNature').value;
document.getElementById('positionNatureName').value = natureMap[nature] || '';
const typeEl = document.getElementById('pos_type');
const typeNameEl = document.getElementById('pos_typeName');
if (typeEl && typeNameEl) {
typeNameEl.value = natureMap[typeEl.value] || '';
}
updatePreview();
}
@@ -210,8 +300,11 @@ export function updateNatureName() {
* 更新職務類別中文名稱
*/
export function updateJobCategoryName() {
const category = document.getElementById('jobCategoryCode').value;
document.getElementById('jobCategoryName').value = jobCategoryMap[category] || '';
const categoryEl = document.getElementById('job_category');
const categoryNameEl = document.getElementById('job_categoryName');
if (categoryEl && categoryNameEl) {
categoryNameEl.value = jobCategoryMap[categoryEl.value] || '';
}
updatePreview();
}
@@ -219,10 +312,12 @@ export function updateJobCategoryName() {
* 修改崗位編號
*/
export function changePositionCode() {
const currentCode = document.getElementById('positionCode').value;
const codeEl = document.getElementById('pos_code');
if (!codeEl) return;
const currentCode = codeEl.value;
const newCode = prompt('請輸入新的崗位編號:', currentCode);
if (newCode && newCode !== currentCode) {
document.getElementById('positionCode').value = newCode;
codeEl.value = newCode;
showToast('崗位編號已更改!');
updatePreview();
}
@@ -232,10 +327,12 @@ export function changePositionCode() {
* 修改職務編號
*/
export function changeJobCode() {
const currentCode = document.getElementById('jobCode').value;
const codeEl = document.getElementById('job_code');
if (!codeEl) return;
const currentCode = codeEl.value;
const newCode = prompt('請輸入新的職務編號:', currentCode);
if (newCode && newCode !== currentCode) {
document.getElementById('jobCode').value = newCode;
codeEl.value = newCode;
showToast('職務編號已更改!');
updatePreview();
}
@@ -271,7 +368,10 @@ export function confirmMajor() {
document.querySelectorAll('#majorModal input[type="checkbox"]:checked').forEach(cb => {
selected.push(cb.value);
});
document.getElementById('majorReq').value = selected.join(', ');
const majorReqEl = document.getElementById('rec_majorReq');
if (majorReqEl) {
majorReqEl.value = selected.join(', ');
}
closeMajorModal();
updatePreview();
}

View File

@@ -132,39 +132,39 @@ export function showCopyableError(options) {
const modalHTML = `
<div id="errorModal" class="modal-overlay show">
<div class="modal" style="max-width: 600px;">
<div class="modal-header" style="background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white;">
<h3>🚨 ${sanitizeHTML(title)}</h3>
<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">
<div style="margin-bottom: 16px;">
<strong>錯誤訊息:</strong>
<p style="color: #e74c3c; font-weight: 500;">${sanitizeHTML(message)}</p>
<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: 16px;">
<strong>詳細資訊:</strong>
<pre style="background: #f8f9fa; padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 0.85rem; max-height: 200px;">${sanitizeHTML(details)}</pre>
<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>請檢查以下項目:</strong>
<ul style="margin: 8px 0; padding-left: 20px;">
${suggestions.map(s => `<li style="margin: 4px 0;">${sanitizeHTML(s)}</li>`).join('')}
<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">
<button class="btn btn-secondary" onclick="copyErrorDetails()">
<svg viewBox="0 0 24 24" style="width: 16px; height: 16px;">
<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)">關閉</button>
<button class="btn btn-primary" onclick="closeErrorModal(this)" style="font-size: 0.85rem; padding: 8px 16px;">關閉</button>
</div>
</div>
</div>

View File

@@ -355,7 +355,8 @@ class LLMConfig:
"temperature": 0.7
}
response = requests.post(url, json=data, headers=headers, timeout=60, verify=False)
# 增加逾時時間到 180 秒,處理大模型較慢的情況
response = requests.post(url, json=data, headers=headers, timeout=180, verify=False)
if response.status_code == 200:
result = response.json()
@@ -364,6 +365,10 @@ class LLMConfig:
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)}"
@@ -395,7 +400,8 @@ class LLMConfig:
"temperature": 0.7
}
response = requests.post(url, json=data, headers=headers, timeout=60, verify=False)
# 增加逾時時間到 180 秒
response = requests.post(url, json=data, headers=headers, timeout=180, verify=False)
if response.status_code == 200:
result = response.json()
@@ -404,6 +410,10 @@ class LLMConfig:
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)}"

View File

@@ -1 +0,0 @@
你好!謝謝你的問候。作為一個人工智能助手,我沒有情緒或身體感受,但我的程式運行得很順利,隨時準備為你提供幫助~ 你呢?今天過得如何?有什麼我可以為你解答的嗎? 😊

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.

View File

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

View File

@@ -32,8 +32,8 @@ cors_origins = os.getenv('CORS_ORIGINS', 'http://localhost:5000,http://127.0.0.1
CORS(app, origins=cors_origins)
# 模擬資料庫
positions_db = {}
jobs_db = {}
HR_position_positions_db = {}
HR_position_jobs_db = {}
# 預設崗位資料
default_positions = {
@@ -76,7 +76,7 @@ default_positions = {
"updatedAt": "2024-01-01T00:00:00"
}
}
positions_db.update(default_positions)
HR_position_positions_db.update(default_positions)
# 預設職務資料
default_jobs = {
@@ -98,7 +98,7 @@ default_jobs = {
"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)
search = request.args.get('search', '', type=str)
filtered = list(positions_db.values())
filtered = list(HR_position_positions_db.values())
if search:
filtered = [p for p in filtered
if search.lower() in p['basicInfo'].get('positionCode', '').lower()
@@ -142,9 +142,9 @@ def get_positions():
@app.route('/api/positions/<position_id>', methods=['GET'])
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': True, 'data': positions_db[position_id]})
return jsonify({'success': True, 'data': HR_position_positions_db[position_id]})
@app.route('/api/positions', methods=['POST'])
def create_position():
@@ -160,7 +160,7 @@ def create_position():
return jsonify({'success': False, 'error': '崗位名稱為必填欄位'}), 400
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
now = datetime.now().isoformat()
@@ -171,7 +171,7 @@ def create_position():
'createdAt': 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
except Exception as e:
return jsonify({'success': False, 'error': f'新增失敗: {str(e)}'}), 500

View File

@@ -14,7 +14,39 @@
--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;

View File

@@ -142,17 +142,25 @@ select {
/* ==================== Buttons ==================== */
.btn {
padding: 12px 24px;
padding: 10px 20px;
border: none;
border-radius: 6px;
border-radius: 8px;
font-family: inherit;
font-size: 0.9rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
display: flex;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s ease;
white-space: nowrap;
text-decoration: none;
line-height: 1.4;
}
.btn:active {
transform: scale(0.98);
}
.btn-primary {
@@ -162,8 +170,9 @@ select {
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(26, 82, 118, 0.4);
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(26, 82, 118, 0.4);
filter: brightness(1.05);
}
.btn-secondary {
@@ -173,28 +182,107 @@ select {
}
.btn-secondary:hover {
background: rgba(26, 82, 118, 0.05);
background: var(--primary);
color: white;
transform: translateY(-1px);
}
.btn-success {
background: linear-gradient(135deg, #27ae60 0%, #1e8449 100%);
color: white;
box-shadow: 0 2px 8px rgba(39, 174, 96, 0.3);
}
.btn-success:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(39, 174, 96, 0.4);
filter: brightness(1.05);
}
.btn-warning {
background: linear-gradient(135deg, #f39c12 0%, #d68910 100%);
color: white;
box-shadow: 0 2px 8px rgba(243, 156, 18, 0.3);
}
.btn-warning:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(243, 156, 18, 0.4);
filter: brightness(1.05);
}
.btn-danger {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
box-shadow: 0 2px 8px rgba(231, 76, 60, 0.3);
}
.btn-danger:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(231, 76, 60, 0.4);
filter: brightness(1.05);
}
.btn-cancel {
background: #f0f3f5;
color: var(--text-secondary);
border: 1px solid var(--border);
}
.btn-cancel:hover {
background: #e5e9ec;
color: var(--text-primary);
border-color: #d0d5da;
}
/* 特殊漸層按鈕 - 儲存至崗位清單 */
.btn-gradient-purple {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.btn-gradient-purple:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
filter: brightness(1.05);
}
.btn svg {
width: 18px;
height: 18px;
fill: currentColor;
flex-shrink: 0;
}
/* 小尺寸按鈕 */
.btn-sm {
padding: 6px 12px;
font-size: 0.8rem;
border-radius: 6px;
}
.btn-sm svg {
width: 14px;
height: 14px;
}
/* 大尺寸按鈕 */
.btn-lg {
padding: 14px 28px;
font-size: 1rem;
border-radius: 10px;
}
.btn-lg svg {
width: 20px;
height: 20px;
}
.btn-lookup {
padding: 10px 16px;
border: 1.5px solid var(--border);
border-radius: 6px;
border-radius: 8px;
background: #f8fafc;
color: var(--text-secondary);
font-size: 0.85rem;
@@ -207,17 +295,19 @@ select {
background: var(--primary);
color: white;
border-color: var(--primary);
transform: translateY(-1px);
}
.btn-icon {
width: 38px;
height: 38px;
padding: 0;
border: 1.5px solid var(--border);
border-radius: 6px;
border-radius: 8px;
background: #f8fafc;
color: var(--text-secondary);
cursor: pointer;
display: flex;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
@@ -227,6 +317,7 @@ select {
background: var(--primary);
color: white;
border-color: var(--primary);
transform: translateY(-1px);
}
.btn-icon svg {
@@ -265,84 +356,139 @@ select {
fill: currentColor;
}
/* ==================== AI Bags (Three Fortune Bags) ==================== */
.ai-bags-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 16px;
/* ==================== AI Helper (AI 幫我想) ==================== */
.ai-helper-container {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 20px;
padding: 16px;
background: linear-gradient(135deg, rgba(155, 89, 182, 0.08) 0%, rgba(142, 68, 173, 0.05) 100%);
border: 1px solid rgba(155, 89, 182, 0.2);
border-radius: var(--radius);
}
.ai-bag {
position: relative;
padding: 8px 12px;
.ai-helper-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
padding: 14px 24px;
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
border-radius: 6px;
color: white;
border: none;
border-radius: 8px;
font-family: inherit;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 15px rgba(155, 89, 182, 0.3);
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(155, 89, 182, 0.3);
text-align: center;
}
.ai-bag:hover {
.ai-helper-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(155, 89, 182, 0.5);
box-shadow: 0 6px 20px rgba(155, 89, 182, 0.4);
}
.ai-bag:active {
.ai-helper-btn:active {
transform: translateY(-1px);
}
.ai-bag .bag-icon {
font-size: 1rem;
margin-bottom: 2px;
}
.ai-bag .bag-title {
color: white;
font-weight: 600;
font-size: 0.75rem;
margin-bottom: 2px;
}
.ai-bag .bag-subtitle {
color: rgba(255, 255, 255, 0.8);
font-size: 0.65rem;
margin-top: 2px;
}
.ai-bag .bag-edit-btn {
position: absolute;
top: 4px;
right: 4px;
background: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 3px;
padding: 2px 4px;
cursor: pointer;
font-size: 0.7rem;
transition: all 0.2s ease;
line-height: 1;
}
.ai-bag .bag-edit-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.ai-bag.loading {
pointer-events: none;
.ai-helper-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
.ai-bag .spinner {
width: 14px;
height: 14px;
.ai-helper-btn svg {
flex-shrink: 0;
}
/* Prompt Editor */
.ai-prompt-editor {
display: flex;
flex-direction: column;
gap: 8px;
}
.prompt-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.prompt-label {
font-size: 0.85rem;
font-weight: 500;
color: var(--text-secondary);
}
.prompt-reset-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
background: rgba(155, 89, 182, 0.1);
border: 1px solid rgba(155, 89, 182, 0.2);
border-radius: 6px;
color: #8e44ad;
cursor: pointer;
transition: all 0.2s ease;
}
.prompt-reset-btn:hover {
background: rgba(155, 89, 182, 0.2);
border-color: rgba(155, 89, 182, 0.3);
}
.prompt-textarea {
width: 100%;
padding: 12px;
border: 1px solid var(--border);
border-radius: 6px;
font-family: 'Monaco', 'Consolas', monospace;
font-size: 0.85rem;
line-height: 1.5;
resize: vertical;
background: white;
color: var(--text-primary);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.prompt-textarea:focus {
outline: none;
border-color: #9b59b6;
box-shadow: 0 0 0 3px rgba(155, 89, 182, 0.15);
}
.prompt-textarea::placeholder {
color: var(--text-secondary);
opacity: 0.6;
}
/* AI Helper Loading State */
.ai-helper-container.loading .ai-helper-btn {
pointer-events: none;
}
.ai-helper-container.loading .ai-helper-btn::after {
content: '';
width: 18px;
height: 18px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 4px auto;
margin-left: 8px;
}
/* Legacy: Keep old styles for backward compatibility */
.ai-bags-container {
display: none; /* Hidden - replaced by ai-helper-container */
}
/* ==================== Old AI Button (Deprecated) ==================== */
@@ -640,3 +786,369 @@ select {
overflow-x: auto;
max-height: 300px;
}
/* ==================== Responsive Design for Components ==================== */
/* Large Tablets (1024px) */
@media (max-width: 1024px) {
.form-grid {
gap: 16px 24px;
}
.form-grid.three-cols {
grid-template-columns: repeat(2, 1fr);
}
.tab-btn {
padding: 14px 24px;
font-size: 0.9rem;
}
.ai-bags-container {
gap: 6px;
}
}
/* Tablets (768px) */
@media (max-width: 768px) {
/* Tabs */
.tabs {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.tab-btn {
padding: 12px 16px;
font-size: 0.85rem;
white-space: nowrap;
}
.tab-content {
padding: 20px 16px;
}
/* Form Grid */
.form-grid,
.form-grid.three-cols {
grid-template-columns: 1fr;
gap: 16px;
}
/* Form Actions */
.form-actions {
flex-direction: column;
gap: 16px;
padding: 16px;
}
.action-buttons {
width: 100%;
flex-wrap: wrap;
justify-content: center;
}
.action-buttons .btn {
flex: 1;
min-width: calc(50% - 6px);
justify-content: center;
}
/* Buttons */
.btn {
padding: 10px 16px;
font-size: 0.85rem;
}
.btn svg {
width: 16px;
height: 16px;
}
.nav-buttons {
gap: 6px;
}
.nav-btn {
width: 32px;
height: 32px;
}
/* AI Bags */
.ai-bags-container {
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}
.ai-bag {
padding: 6px 8px;
}
.ai-bag .bag-title {
font-size: 0.7rem;
}
.ai-bag .bag-subtitle {
font-size: 0.6rem;
}
.ai-bag .bag-icon {
font-size: 0.9rem;
}
/* Toggle */
.toggle-group {
flex-wrap: wrap;
}
.toggle-switch {
width: 46px;
height: 24px;
}
.toggle-slider:before {
height: 18px;
width: 18px;
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(22px);
}
/* Checkbox */
.checkbox-group {
grid-template-columns: 1fr;
}
/* Modal */
.modal {
width: 95%;
max-width: none;
margin: 16px;
}
.modal-header {
padding: 16px;
}
.modal-header h3 {
font-size: 1rem;
}
.modal-body {
padding: 16px;
}
.modal-footer {
padding: 12px 16px;
flex-wrap: wrap;
}
.modal-footer .btn {
flex: 1;
min-width: calc(50% - 6px);
}
/* Toast */
.toast {
top: 12px;
right: 12px;
left: 12px;
padding: 12px 16px;
font-size: 0.9rem;
}
.toast svg {
width: 20px;
height: 20px;
}
/* Data Preview */
.data-preview {
padding: 16px;
margin-top: 16px;
}
.data-preview pre {
padding: 12px;
font-size: 0.75rem;
max-height: 200px;
}
}
/* Mobile Phones (480px) */
@media (max-width: 480px) {
/* Tabs */
.tab-btn {
padding: 10px 12px;
font-size: 0.8rem;
}
.tab-content {
padding: 16px 12px;
}
/* Form */
.form-group label {
font-size: 0.8rem;
}
input[type="text"],
input[type="number"],
input[type="date"],
select,
textarea {
padding: 8px 12px;
font-size: 0.85rem;
}
.input-wrapper {
flex-direction: column;
align-items: stretch;
}
.btn-lookup {
padding: 8px 12px;
font-size: 0.8rem;
}
/* Form Actions */
.form-actions {
padding: 12px;
}
.action-buttons {
flex-direction: column;
}
.action-buttons .btn {
width: 100%;
min-width: 100%;
}
/* Buttons */
.btn {
padding: 10px 14px;
font-size: 0.8rem;
gap: 6px;
}
.btn-icon {
width: 34px;
height: 34px;
}
/* AI Bags */
.ai-bags-container {
grid-template-columns: 1fr 1fr 1fr;
gap: 4px;
}
.ai-bag {
padding: 6px 4px;
}
.ai-bag .bag-icon {
font-size: 0.85rem;
}
.ai-bag .bag-title {
font-size: 0.65rem;
}
.ai-bag .bag-subtitle {
display: none;
}
.ai-bag .bag-edit-btn {
padding: 1px 3px;
font-size: 0.6rem;
}
/* AI Generate Button */
.ai-generate-btn {
padding: 10px 16px;
font-size: 0.85rem;
}
/* Toggle */
.toggle-label {
font-size: 0.8rem;
}
/* Checkbox */
.checkbox-item {
padding: 8px 10px;
}
.checkbox-item input {
width: 16px;
height: 16px;
}
.checkbox-item label {
font-size: 0.85rem;
}
/* Modal */
.modal {
margin: 8px;
width: calc(100% - 16px);
}
.modal-header {
padding: 12px;
}
.modal-header h3 {
font-size: 0.95rem;
}
.modal-close {
width: 28px;
height: 28px;
}
.modal-body {
padding: 12px;
}
.modal-footer {
padding: 10px 12px;
gap: 8px;
}
.modal-footer .btn {
padding: 8px 12px;
font-size: 0.8rem;
}
/* Toast */
.toast {
top: 8px;
right: 8px;
left: 8px;
padding: 10px 14px;
font-size: 0.85rem;
gap: 8px;
}
.toast svg {
width: 18px;
height: 18px;
}
/* Data Preview */
.data-preview {
padding: 12px;
}
.data-preview h4 {
font-size: 0.85rem;
}
.data-preview pre {
padding: 10px;
font-size: 0.7rem;
max-height: 150px;
}
}

View File

@@ -1,25 +1,37 @@
/**
* Layout Styles - 布局樣式
* 包含容器、整體布局、Header、模組切換器
* Two-Column Layout: 左側導航 + 右側內容
*/
/* ==================== Container ==================== */
/* ==================== Main Layout ==================== */
.app-container {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
display: flex;
min-height: 100vh;
background: var(--bg);
}
/* ==================== User Info Bar ==================== */
.user-info-bar {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: var(--radius);
padding: 12px 20px;
margin-bottom: 20px;
/* ==================== Sidebar (左側導航欄) ==================== */
.sidebar {
width: 240px;
min-width: 240px;
background: linear-gradient(180deg, #1a1f2e 0%, #2d3748 100%);
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow);
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 {
@@ -30,117 +42,147 @@
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
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: 1rem;
font-size: 0.95rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-role {
font-size: 0.85rem;
opacity: 0.9;
font-size: 0.75rem;
opacity: 0.7;
color: #a0aec0;
}
.logout-btn {
padding: 8px 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
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.9rem;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.logout-btn:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-2px);
}
.logout-btn svg {
width: 18px;
height: 18px;
fill: currentColor;
}
/* ==================== Module Selector ==================== */
.module-selector {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.module-btn {
flex: 1;
min-width: 200px;
padding: 14px 20px;
border: 2px solid var(--border);
border-radius: var(--radius);
background: var(--bg-card);
font-family: inherit;
font-size: 0.95rem;
font-weight: 500;
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: all 0.3s ease;
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 {
border-color: var(--primary-light);
color: var(--primary);
transform: translateY(-2px);
box-shadow: var(--shadow);
background: rgba(255, 255, 255, 0.08);
color: #e2e8f0;
}
.module-btn.active {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-color: var(--primary);
background: linear-gradient(135deg, var(--module-position) 0%, var(--module-position-dark) 100%);
color: white;
box-shadow: 0 4px 15px rgba(26, 82, 118, 0.3);
box-shadow: 0 4px 12px var(--module-position-shadow);
}
.module-btn.active.job-active {
background: linear-gradient(135deg, var(--accent) 0%, #d35400 100%);
border-color: var(--accent);
box-shadow: 0 4px 15px rgba(230, 126, 34, 0.3);
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(--green) 0%, var(--green-dark) 100%);
border-color: var(--green);
box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3);
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: 22px;
height: 22px;
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 {
@@ -159,17 +201,29 @@
gap: 16px;
margin-bottom: 24px;
padding: 20px 28px;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
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(--accent) 0%, #d35400 100%);
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(--green) 0%, var(--green-dark) 100%);
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 {
@@ -255,3 +309,214 @@
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;
}
}

View File

@@ -3,25 +3,334 @@
* 包含各個頁籤/模組的特定樣式需求
*/
/* ==================== 共用模組樣式 ==================== */
/* 模組頂部工具列 */
.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;
}
}

View File

@@ -1,56 +1,244 @@
/**
* Utilities Styles - 工具類別與響應式設計
* 包含快速工具類別、響應式斷點
* Utilities Styles - 工具類別與額外響應式設計
* 包含快速工具類別、表格響應式、額外斷點調整
*/
/* ==================== Utility Classes ==================== */
/* (可按需添加工具類別,如 .mt-1, .text-center 等) */
/* ==================== Responsive Design ==================== */
@media (max-width: 768px) {
/* Form Grid */
.form-grid,
.form-grid.three-cols {
grid-template-columns: 1fr;
}
/* Display */
.d-none { display: none !important; }
.d-block { display: block !important; }
.d-flex { display: flex !important; }
.d-grid { display: grid !important; }
/* Form Actions */
.form-actions {
flex-direction: column;
gap: 16px;
}
/* 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; }
.action-buttons {
/* 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;
}
.btn {
justify-content: center;
}
/* Module Selector */
.module-selector {
flex-direction: column;
}
.module-btn {
min-width: 100%;
}
/* User Info Bar */
.user-info-bar {
flex-direction: column;
gap: 12px;
text-align: center;
}
.user-info {
justify-content: center;
}
.logout-btn {
.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

@@ -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 助理