Files
hr-position-system/index.html
DonaldFang 方士碩 b2584772c4 feat: 新增崗位描述與清單整合功能 v2.1
主要功能更新:
- 崗位描述保存功能:保存後資料寫入資料庫
- 崗位清單自動刷新:切換模組時自動載入最新資料
- 崗位清單檢視功能:點擊「檢視」按鈕載入對應描述
- 管理者頁面擴充:新增崗位資料管理與匯出功能
- CSV 批次匯入:支援崗位與職務資料批次匯入

後端 API 新增:
- Position Description CRUD APIs
- Position List Query & Export APIs
- CSV Template Download & Import APIs

文件更新:
- SDD.md 更新至版本 2.1
- README.md 更新功能說明與版本歷史

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 12:46:36 +08:00

3690 lines
185 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HR 基礎資料維護系統</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary: #1a5276;
--primary-light: #2980b9;
--primary-dark: #0e3a53;
--accent: #e67e22;
--accent-light: #f39c12;
--green: #27ae60;
--green-dark: #1e8449;
--bg-main: #f4f6f9;
--bg-card: #ffffff;
--border: #d5dbdf;
--text-primary: #2c3e50;
--text-secondary: #5d6d7e;
--success: #27ae60;
--warning: #f39c12;
--danger: #e74c3c;
--shadow: 0 4px 20px rgba(26, 82, 118, 0.12);
--radius: 8px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Noto Sans TC', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #e8f4fc 0%, #f4f6f9 100%);
min-height: 100vh;
color: var(--text-primary);
}
.app-container { max-width: 1200px; margin: 0 auto; padding: 24px; }
/* 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;
}
.module-btn:hover {
border-color: var(--primary-light);
color: var(--primary);
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.module-btn.active {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-color: var(--primary);
color: white;
box-shadow: 0 4px 15px rgba(26, 82, 118, 0.3);
}
.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);
}
.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);
}
.module-btn svg { width: 22px; height: 22px; fill: currentColor; }
.module-content { display: none; }
.module-content.active { display: block; animation: fadeIn 0.3s ease; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.app-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding: 20px 28px;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.app-header.job-header {
background: linear-gradient(135deg, var(--accent) 0%, #d35400 100%);
}
.app-header.desc-header {
background: linear-gradient(135deg, var(--green) 0%, var(--green-dark) 100%);
}
.app-header .icon {
width: 48px; height: 48px;
background: rgba(255,255,255,0.15);
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
}
.app-header .icon svg { width: 28px; height: 28px; fill: #ffffff; }
.app-header h1 { color: #ffffff; font-size: 1.5rem; font-weight: 600; }
.app-header .subtitle { color: rgba(255,255,255,0.7); font-size: 0.875rem; margin-top: 2px; }
.form-card {
background: var(--bg-card);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
/* Tabs */
.tabs { display: flex; background: #f8fafc; border-bottom: 2px solid var(--border); }
.tab-btn {
padding: 16px 32px;
border: none;
background: transparent;
font-family: inherit;
font-size: 0.95rem;
font-weight: 500;
color: var(--text-secondary);
cursor: pointer;
position: relative;
transition: all 0.3s ease;
}
.tab-btn:hover { color: var(--primary); background: rgba(26, 82, 118, 0.05); }
.tab-btn.active { color: var(--primary); background: var(--bg-card); }
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -2px; left: 0; right: 0;
height: 3px;
background: var(--primary);
border-radius: 2px 2px 0 0;
}
.tab-content { display: none; padding: 28px; animation: fadeIn 0.3s ease; }
.tab-content.active { display: block; }
/* Form Grid */
.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px 32px; }
.form-grid.three-cols { grid-template-columns: repeat(3, 1fr); }
.form-group { display: flex; flex-direction: column; gap: 6px; }
.form-group.full-width { grid-column: 1 / -1; }
.form-group label {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 4px;
}
.form-group label .required { color: var(--danger); font-weight: 600; }
.input-wrapper { display: flex; gap: 8px; align-items: center; }
input[type="text"],
input[type="number"],
input[type="date"],
select,
textarea {
width: 100%;
padding: 10px 14px;
border: 1.5px solid var(--border);
border-radius: 6px;
font-family: inherit;
font-size: 0.9rem;
color: var(--text-primary);
background: #fafbfc;
transition: all 0.2s ease;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--primary-light);
background: #ffffff;
box-shadow: 0 0 0 3px rgba(41, 128, 185, 0.1);
}
input:read-only {
background: #f0f3f5;
color: var(--text-secondary);
cursor: default;
}
textarea { min-height: 100px; resize: vertical; }
select {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%235d6d7e' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 36px;
}
.btn-lookup {
padding: 10px 16px;
border: 1.5px solid var(--border);
border-radius: 6px;
background: #f8fafc;
color: var(--text-secondary);
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.btn-lookup:hover {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.btn-icon {
width: 38px; height: 38px;
border: 1.5px solid var(--border);
border-radius: 6px;
background: #f8fafc;
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.btn-icon:hover {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.btn-icon svg { width: 18px; height: 18px; fill: currentColor; }
/* Toggle Switch */
.toggle-group { display: flex; align-items: center; gap: 12px; }
.toggle-switch { position: relative; width: 52px; height: 28px; }
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #ccc;
transition: 0.3s;
border-radius: 28px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 22px; width: 22px;
left: 3px; bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.toggle-switch input:checked + .toggle-slider { background-color: var(--success); }
.toggle-switch input:checked + .toggle-slider:before { transform: translateX(24px); }
.toggle-label { font-size: 0.9rem; color: var(--text-secondary); }
/* Section */
.section-box {
background: #f8fafc;
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 20px;
}
.section-header {
padding: 12px 16px;
background: linear-gradient(135deg, #e8f4fc 0%, #f0f4f8 100%);
border-bottom: 1px solid var(--border);
font-weight: 600;
font-size: 0.9rem;
color: var(--primary);
}
.section-header.green {
background: linear-gradient(135deg, #e8f8f0 0%, #f0f8f4 100%);
color: var(--green-dark);
}
.section-body { padding: 20px; }
.section-divider {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 12px;
margin: 8px 0;
}
.section-divider span {
font-size: 0.85rem;
font-weight: 600;
color: var(--primary);
white-space: nowrap;
}
.section-divider::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(to right, var(--border), transparent);
}
/* Actions */
.form-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 28px;
background: #f8fafc;
border-top: 1px solid var(--border);
}
.nav-buttons { display: flex; gap: 8px; }
.nav-btn {
width: 36px; height: 36px;
border: 1.5px solid var(--border);
border-radius: 6px;
background: white;
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.nav-btn:hover { border-color: var(--primary); color: var(--primary); }
.nav-btn svg { width: 16px; height: 16px; fill: currentColor; }
.action-buttons { display: flex; gap: 12px; }
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-family: inherit;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
box-shadow: 0 2px 8px rgba(26, 82, 118, 0.3);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(26, 82, 118, 0.4);
}
.btn-secondary {
background: white;
color: var(--primary);
border: 1.5px solid var(--primary);
}
.btn-secondary:hover { background: rgba(26, 82, 118, 0.05); }
.btn-cancel { background: #f0f3f5; color: var(--text-secondary); }
.btn-cancel:hover { background: #e5e9ec; }
.btn svg { width: 18px; height: 18px; fill: currentColor; }
/* Toast */
.toast {
position: fixed;
top: 24px; right: 24px;
padding: 16px 24px;
background: var(--success);
color: white;
border-radius: var(--radius);
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
display: flex;
align-items: center;
gap: 12px;
transform: translateX(120%);
transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
z-index: 1000;
}
.toast.show { transform: translateX(0); }
.toast svg { width: 24px; height: 24px; fill: currentColor; }
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 100;
backdrop-filter: blur(4px);
}
.modal-overlay.show { display: flex; }
.modal {
background: white;
border-radius: var(--radius);
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
width: 90%;
max-width: 500px;
animation: modalIn 0.3s ease;
}
@keyframes modalIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.modal-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 { font-size: 1.1rem; font-weight: 600; }
.modal-close {
width: 32px; height: 32px;
border: none;
background: #f0f3f5;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.modal-close:hover { background: var(--danger); color: white; }
.modal-body { padding: 24px; }
.modal-footer {
padding: 16px 24px;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 12px;
}
.checkbox-group { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border: 1.5px solid var(--border);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.checkbox-item:hover {
border-color: var(--primary-light);
background: rgba(41, 128, 185, 0.05);
}
.checkbox-item input { width: 18px; height: 18px; accent-color: var(--primary); }
.checkbox-item label { cursor: pointer; font-size: 0.9rem; }
/* Confidential Field */
.confidential-field { position: relative; }
.confidential-field input { padding-left: 32px; }
.confidential-field::before {
content: '🔒';
position: absolute;
left: 10px; top: 50%;
transform: translateY(-50%);
font-size: 14px;
}
/* Numbered Textarea */
.numbered-textarea {
font-family: inherit;
line-height: 1.8;
}
/* Data Preview */
.data-preview {
margin-top: 24px;
padding: 20px;
background: #f8fafc;
border-radius: var(--radius);
border: 1px solid var(--border);
}
.data-preview h4 { font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 12px; }
.data-preview pre {
font-family: 'Monaco', 'Consolas', monospace;
font-size: 0.8rem;
background: #2c3e50;
color: #ecf0f1;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
max-height: 300px;
}
/* AI Button */
.ai-generate-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
margin-bottom: 24px;
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
color: white;
border: none;
border-radius: var(--radius);
font-family: inherit;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 15px rgba(155, 89, 182, 0.3);
transition: all 0.3s ease;
}
.ai-generate-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(155, 89, 182, 0.4);
}
.ai-generate-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
.ai-generate-btn svg {
width: 20px;
height: 20px;
fill: currentColor;
}
.ai-generate-btn .spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Responsive */
@media (max-width: 768px) {
.form-grid, .form-grid.three-cols { grid-template-columns: 1fr; }
.form-actions { flex-direction: column; gap: 16px; }
.action-buttons { width: 100%; flex-direction: column; }
.btn { justify-content: center; }
.module-selector { flex-direction: column; }
.module-btn { min-width: 100%; }
}
</style>
<script src="csv_utils.js"></script>
</head>
<body>
<div class="app-container">
<!-- Module Selector -->
<div class="module-selector">
<button class="module-btn active" data-module="position">
<svg viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
崗位基礎資料
</button>
<button class="module-btn" data-module="job">
<svg viewBox="0 0 24 24"><path d="M20 6h-4V4c0-1.11-.89-2-2-2h-4c-1.11 0-2 .89-2 2v2H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-6 0h-4V4h4v2z"/></svg>
職務基礎資料
</button>
<button class="module-btn" data-module="deptfunction">
<svg viewBox="0 0 24 24"><path d="M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z"/></svg>
部門職責
</button>
<button class="module-btn" data-module="jobdesc">
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
崗位描述
</button>
<button class="module-btn" data-module="positionlist">
<svg viewBox="0 0 24 24"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg>
崗位清單
</button>
<button class="module-btn" data-module="admin">
<svg viewBox="0 0 24 24"><path d="M19.14,12.94c0.04-0.31,0.06-0.63,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>
管理者頁面
</button>
</div>
<!-- ==================== 崗位基礎資料模組 ==================== -->
<div class="module-content active" id="module-position">
<header class="app-header">
<div class="icon">
<svg viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
</div>
<div>
<h1>崗位基礎資料維護</h1>
<div class="subtitle">Position Master Data Management</div>
</div>
</header>
<div class="form-card">
<div class="tabs">
<button class="tab-btn active" data-tab="position-basic">基礎資料</button>
<button class="tab-btn" data-tab="position-recruit">招聘要求資料</button>
</div>
<form id="positionForm">
<div class="tab-content active" id="tab-position-basic">
<button type="button" class="ai-generate-btn" onclick="generatePositionBasic()">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<span>✨ I'm feeling lucky</span>
</button>
<div class="form-grid">
<!-- 事業體 -->
<div class="form-group">
<label>事業體 (Business Unit)</label>
<select id="businessUnit" name="businessUnit">
<option value="">請選擇</option>
<option value="SBU">SBU - 銷售事業體</option>
<option value="MBU">MBU - 製造事業體</option>
<option value="HQBU">HQBU - 總部事業體</option>
<option value="ITBU">ITBU - IT事業體</option>
<option value="HRBU">HRBU - HR事業體</option>
<option value="ACCBU">ACCBU - 會計事業體</option>
</select>
</div>
<!-- 處級單位 -->
<div class="form-group">
<label>處級單位 (Division)</label>
<input type="text" id="division" name="division" placeholder="選填">
</div>
<!-- 部級單位 -->
<div class="form-group">
<label>部級單位 (Department)</label>
<input type="text" id="department" name="department" placeholder="選填">
</div>
<!-- 課級單位 -->
<div class="form-group">
<label>課級單位 (Section)</label>
<input type="text" id="section" name="section" placeholder="選填">
</div>
<div class="form-group">
<label><span class="required">*</span> 崗位編號</label>
<div class="input-wrapper">
<input type="text" id="positionCode" name="positionCode" required placeholder="請輸入崗位編號">
<button type="button" class="btn-lookup" onclick="changePositionCode()">更改崗位編號</button>
</div>
</div>
<div class="form-group">
<label>生效日期</label>
<input type="date" id="effectiveDate" name="effectiveDate" value="2001-01-01">
</div>
<div class="form-group">
<label><span class="required">*</span> 崗位名稱</label>
<input type="text" id="positionName" name="positionName" required placeholder="請輸入崗位名稱">
</div>
<div class="form-group">
<label>崗位級別</label>
<select id="positionLevel" name="positionLevel">
<option value="">請選擇</option>
<option value="L1">L1 - 基層員工</option>
<option value="L2">L2 - 資深員工</option>
<option value="L3">L3 - 主管</option>
<option value="L4">L4 - 經理</option>
<option value="L5">L5 - 總監</option>
<option value="L6">L6 - 副總</option>
<option value="L7">L7 - 總經理</option>
</select>
</div>
<div class="form-group">
<label>崗位類別</label>
<select id="positionCategory" name="positionCategory" onchange="updateCategoryName()">
<option value="">請選擇</option>
<option value="01">01</option>
<option value="02">02</option>
<option value="03">03</option>
<option value="04">04</option>
</select>
</div>
<div class="form-group">
<label>崗位類別名稱</label>
<input type="text" id="positionCategoryName" name="positionCategoryName" readonly placeholder="自動帶出">
</div>
<div class="form-group">
<label>崗位性質</label>
<select id="positionNature" name="positionNature" onchange="updateNatureName()">
<option value="">請選擇</option>
<option value="FT">全職</option>
<option value="PT">兼職</option>
<option value="CT">約聘</option>
<option value="IN">實習</option>
</select>
</div>
<div class="form-group">
<label>崗位性質名稱</label>
<input type="text" id="positionNatureName" name="positionNatureName" readonly placeholder="自動帶出">
</div>
<div class="form-group">
<label>編制人數</label>
<input type="number" id="headcount" name="headcount" min="0" placeholder="請輸入編制人數">
</div>
<div class="form-group"></div>
<div class="form-group full-width">
<label>崗位描述(條列式說明)</label>
<textarea id="positionDesc" name="positionDesc" placeholder="請以條列式輸入崗位描述,例如:&#10;1. 負責系統開發與維護&#10;2. 撰寫技術文件&#10;3. 參與專案規劃與執行" rows="6"></textarea>
</div>
<div class="form-group full-width">
<label>崗位備注(條列式說明)</label>
<textarea id="positionRemark" name="positionRemark" placeholder="請以條列式輸入備注說明,例如:&#10;1. 需具備良好溝通能力&#10;2. 可配合加班&#10;3. 其他注意事項" rows="6"></textarea>
</div>
</div>
</div>
<div class="tab-content" id="tab-position-recruit">
<button type="button" class="ai-generate-btn" onclick="generatePositionRecruit()">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<span>✨ I'm feeling lucky</span>
</button>
<div class="form-grid">
<div class="form-group">
<label>最低學歷</label>
<select id="minEducation" name="minEducation">
<option value="">請選擇</option>
<option value="HS">高中/職</option>
<option value="JC">專科</option>
<option value="BA">大學</option>
<option value="MA">碩士</option>
<option value="PHD">博士</option>
</select>
</div>
<div class="form-group">
<label>要求性別</label>
<select id="requiredGender" name="requiredGender">
<option value="">不限</option>
<option value="M"></option>
<option value="F"></option>
</select>
</div>
<div class="form-group">
<label>薪酬范圍</label>
<select id="salaryRange" name="salaryRange">
<option value="">請選擇</option>
<option value="A">30,000 以下</option>
<option value="B">30,000 - 50,000</option>
<option value="C">50,000 - 80,000</option>
<option value="D">80,000 - 120,000</option>
<option value="E">120,000 以上</option>
<option value="N">面議</option>
</select>
</div>
<div class="form-group">
<label>工作經驗</label>
<select id="workExperience" name="workExperience">
<option value="">請選擇</option>
<option value="0">不限</option>
<option value="1">1年以上</option>
<option value="3">3年以上</option>
<option value="5">5年以上</option>
<option value="10">10年以上</option>
</select>
</div>
<div class="form-group">
<label>最小年齡</label>
<input type="number" id="minAge" name="minAge" min="18" max="65" placeholder="歲">
</div>
<div class="form-group">
<label>最大年齡</label>
<input type="number" id="maxAge" name="maxAge" min="18" max="65" placeholder="歲">
</div>
<div class="form-group">
<label>工作性質</label>
<select id="jobType" name="jobType">
<option value="">請選擇</option>
<option value="FT">全職</option>
<option value="PT">兼職</option>
<option value="CT">約聘</option>
<option value="DP">派遣</option>
</select>
</div>
<div class="form-group">
<label>職位名稱</label>
<input type="text" id="jobTitle" name="jobTitle" placeholder="請輸入職位名稱">
</div>
<div class="form-group">
<label>招聘職位</label>
<select id="recruitPosition" name="recruitPosition">
<option value="">請選擇</option>
<option value="ENG">工程師</option>
<option value="MGR">經理</option>
<option value="AST">助理</option>
<option value="OP">作業員</option>
<option value="SAL">業務</option>
</select>
</div>
<div class="form-group">
<label>上級崗位編號</label>
<input type="text" id="superiorPosition" name="superiorPosition" placeholder="請輸入上級崗位編號">
</div>
<div class="form-group full-width">
<label>職位描述</label>
<textarea id="jobDesc" name="jobDesc" placeholder="請輸入職位描述..." rows="3"></textarea>
</div>
<div class="form-group full-width">
<label>崗位要求</label>
<textarea id="positionReq" name="positionReq" placeholder="請輸入崗位要求..." rows="3"></textarea>
</div>
<div class="section-divider"><span>技能與專業要求</span></div>
<div class="form-group">
<label>職稱要求</label>
<select id="titleReq" name="titleReq">
<option value="">請選擇</option>
<option value="NONE">不限</option>
<option value="CERT">相關證照</option>
<option value="LIC">專業執照</option>
</select>
</div>
<div class="form-group">
<label>專業要求</label>
<div class="input-wrapper">
<input type="text" id="majorReq" name="majorReq" readonly placeholder="點擊選擇專業">
<button type="button" class="btn-lookup" onclick="openMajorModal()">專業要求</button>
</div>
</div>
<div class="form-group">
<label>技能要求</label>
<input type="text" id="skillReq" name="skillReq" placeholder="如Excel, Python, SAP...">
</div>
<div class="form-group">
<label>語言要求</label>
<input type="text" id="langReq" name="langReq" placeholder="如英文中級、日文N2...">
</div>
<div class="form-group full-width">
<label>其他要求</label>
<input type="text" id="otherReq" name="otherReq" placeholder="請輸入其他要求">
</div>
<div class="form-group full-width">
<label>備注說明</label>
<textarea id="recruitRemark" name="recruitRemark" placeholder="請輸入備注說明..." rows="4"></textarea>
</div>
</div>
</div>
</form>
<div class="form-actions">
<div class="nav-buttons">
<button class="nav-btn" title="第一筆"><svg viewBox="0 0 24 24"><path d="M18.41 16.59L13.82 12l4.59-4.59L17 6l-6 6 6 6 1.41-1.41zM6 6h2v12H6V6z"/></svg></button>
<button class="nav-btn" title="上一筆"><svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg></button>
<button class="nav-btn" title="下一筆"><svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg></button>
<button class="nav-btn" title="最後一筆"><svg viewBox="0 0 24 24"><path d="M5.59 7.41L10.18 12l-4.59 4.59L7 18l6-6-6-6-1.41 1.41zM16 6h2v12h-2V6z"/></svg></button>
</div>
<!-- CSV 匯入匯出按鈕 -->
<div class="csv-buttons" style="margin-bottom: 15px; display: flex; gap: 10px;">
<button type="button" class="btn btn-secondary" onclick="downloadPositionCSVTemplate()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
下載範本
</button>
<button type="button" class="btn btn-secondary" onclick="exportPositionsCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
匯出 CSV
</button>
<button type="button" class="btn btn-secondary" onclick="importPositionsCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"/></svg>
匯入 CSV
</button>
<input type="file" id="positionCSVInput" accept=".csv" style="display: none;" onchange="handlePositionCSVImport(event)">
</div>
<div class="action-buttons">
<button type="button" class="btn btn-primary" onclick="savePositionAndExit()">
<svg viewBox="0 0 24 24"><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>
保存并退出(S)
</button>
<button type="button" class="btn btn-secondary" onclick="savePositionAndNew()">
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
保存并新增(N)
</button>
<button type="button" class="btn btn-cancel" onclick="cancelPositionForm()">取消</button>
</div>
</div>
</div>
</div>
<!-- ==================== 職務基礎資料模組 ==================== -->
<div class="module-content" id="module-job">
<header class="app-header job-header">
<div class="icon">
<svg viewBox="0 0 24 24"><path d="M20 6h-4V4c0-1.11-.89-2-2-2h-4c-1.11 0-2 .89-2 2v2H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-6 0h-4V4h4v2z"/></svg>
</div>
<div>
<h1>職務基礎資料維護</h1>
<div class="subtitle">Job Title Master Data Management</div>
</div>
</header>
<div class="form-card">
<div class="tabs">
<button class="tab-btn active" data-tab="job-basic">基礎資料</button>
</div>
<form id="jobForm">
<div class="tab-content active" id="tab-job-basic">
<button type="button" class="ai-generate-btn" onclick="generateJobBasic()">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<span>✨ I'm feeling lucky</span>
</button>
<div class="form-grid">
<div class="form-group">
<label><span class="required">*</span> 職務類別編號</label>
<select id="jobCategoryCode" name="jobCategoryCode" required onchange="updateJobCategoryName()">
<option value="">請選擇</option>
<option value="MGR">管理職</option>
<option value="TECH">技術職</option>
<option value="SALE">業務職</option>
<option value="ADMIN">行政職</option>
<option value="RD">研發職</option>
<option value="PROD">生產職</option>
</select>
</div>
<div class="form-group">
<label>職務類別名稱</label>
<input type="text" id="jobCategoryName" name="jobCategoryName" readonly placeholder="自動帶出">
</div>
<div class="form-group">
<label><span class="required">*</span> 職務編號</label>
<div class="input-wrapper">
<input type="text" id="jobCode" name="jobCode" required placeholder="請輸入職務編號">
<button type="button" class="btn-lookup" onclick="changeJobCode()">更改職務編號</button>
</div>
</div>
<div class="form-group"></div>
<div class="form-group">
<label><span class="required">*</span> 職務名稱</label>
<input type="text" id="jobName" name="jobName" required placeholder="請輸入職務名稱">
</div>
<div class="form-group">
<label>職務英文</label>
<input type="text" id="jobNameEn" name="jobNameEn" placeholder="請輸入職務英文名稱">
</div>
<div class="form-group">
<label>生效日期</label>
<input type="date" id="jobEffectiveDate" name="jobEffectiveDate">
</div>
<div class="form-group">
<label>編制人數</label>
<input type="number" id="jobHeadcount" name="jobHeadcount" min="0" placeholder="請輸入人數">
</div>
<div class="form-group">
<label>排列順序</label>
<input type="number" id="jobSortOrder" name="jobSortOrder" min="0" placeholder="請輸入排序">
</div>
<div class="form-group"></div>
<div class="form-group full-width">
<label>備注說明</label>
<textarea id="jobRemark" name="jobRemark" placeholder="請輸入備注說明..." rows="4"></textarea>
</div>
<div class="section-divider"><span>職務屬性設定</span></div>
<div class="form-group">
<label>職務層級</label>
<div class="confidential-field">
<input type="text" id="jobLevel" name="jobLevel" placeholder="如:*保密*">
</div>
</div>
<div class="form-group"></div>
<div class="form-group">
<label>是否有全勤</label>
<div class="toggle-group">
<label class="toggle-switch">
<input type="checkbox" id="hasAttendanceBonus" name="hasAttendanceBonus">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label" id="attendanceLabel"></span>
</div>
</div>
<div class="form-group">
<label>是否住房補貼</label>
<div class="toggle-group">
<label class="toggle-switch">
<input type="checkbox" id="hasHousingAllowance" name="hasHousingAllowance">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label" id="housingLabel"></span>
</div>
</div>
</div>
</div>
</form>
<div class="form-actions">
<div class="nav-buttons">
<button class="nav-btn" title="第一筆"><svg viewBox="0 0 24 24"><path d="M18.41 16.59L13.82 12l4.59-4.59L17 6l-6 6 6 6 1.41-1.41zM6 6h2v12H6V6z"/></svg></button>
<button class="nav-btn" title="上一筆"><svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg></button>
<button class="nav-btn" title="下一筆"><svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg></button>
<button class="nav-btn" title="最後一筆"><svg viewBox="0 0 24 24"><path d="M5.59 7.41L10.18 12l-4.59 4.59L7 18l6-6-6-6-1.41 1.41zM16 6h2v12h-2V6z"/></svg></button>
</div>
<!-- CSV 匯入匯出按鈕 -->
<div class="csv-buttons" style="margin-bottom: 15px; display: flex; gap: 10px;">
<button type="button" class="btn btn-secondary" onclick="downloadJobCSVTemplate()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
下載範本
</button>
<button type="button" class="btn btn-secondary" onclick="exportJobsCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
匯出 CSV
</button>
<button type="button" class="btn btn-secondary" onclick="importJobsCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"/></svg>
匯入 CSV
</button>
<input type="file" id="jobCSVInput" accept=".csv" style="display: none;" onchange="handleJobCSVImport(event)">
</div>
<div class="action-buttons">
<button type="button" class="btn btn-primary" onclick="saveJobAndExit()">
<svg viewBox="0 0 24 24"><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>
保存并退出(S)
</button>
<button type="button" class="btn btn-secondary" onclick="saveJobAndNew()">
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
保存并新增(N)
</button>
<button type="button" class="btn btn-cancel" onclick="cancelJobForm()">取消</button>
</div>
</div>
</div>
</div>
<!-- ==================== 部門職責模組 ==================== -->
<div class="module-content" id="module-deptfunction">
<header class="app-header">
<div class="icon">
<svg viewBox="0 0 24 24"><path d="M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z"/></svg>
</div>
<div>
<h1>部門職責維護</h1>
<div class="subtitle">Department Function Management</div>
</div>
</header>
<div class="form-card">
<form id="deptFunctionForm">
<div class="tab-content active">
<button type="button" class="ai-generate-btn" onclick="generateDeptFunction()">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<span>✨ I'm feeling lucky</span>
</button>
<div class="csv-buttons" style="margin-bottom: 15px;">
<button type="button" class="btn btn-secondary" onclick="importDeptFunctionCSV()">匯入 CSV</button>
<button type="button" class="btn btn-secondary" onclick="exportDeptFunctionCSV()">匯出 CSV</button>
<input type="file" id="deptFunctionCsvInput" accept=".csv" style="display: none;" onchange="handleDeptFunctionCSVImport(event)">
</div>
<div class="form-row">
<div class="form-group">
<label>部門職責編號 <span class="required">*</span></label>
<input type="text" id="deptFunctionCode" name="deptFunctionCode" required placeholder="例如: DF-001">
</div>
<div class="form-group">
<label>部門職責名稱 <span class="required">*</span></label>
<input type="text" id="deptFunctionName" name="deptFunctionName" required placeholder="例如: 軟體研發部職責">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>事業體 (Business Unit) <span class="required">*</span></label>
<select id="deptFunctionBU" name="deptFunctionBU" required>
<option value="">-- 請選擇 --</option>
<option value="SBU">SBU - 業務事業體</option>
<option value="MBU">MBU - 製造事業體</option>
<option value="HQBU">HQBU - 總部事業體</option>
<option value="ITBU">ITBU - 資訊事業體</option>
<option value="HRBU">HRBU - 人資事業體</option>
<option value="ACCBU">ACCBU - 財會事業體</option>
</select>
</div>
<div class="form-group">
<label>部門名稱 <span class="required">*</span></label>
<input type="text" id="deptFunctionDept" name="deptFunctionDept" required placeholder="例如: 軟體研發部">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>部門主管職稱</label>
<input type="text" id="deptManager" name="deptManager" placeholder="例如: 部門經理">
</div>
<div class="form-group">
<label>生效日期 <span class="required">*</span></label>
<input type="date" id="deptFunctionEffectiveDate" name="deptFunctionEffectiveDate" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>部門人數上限</label>
<input type="number" id="deptHeadcount" name="deptHeadcount" min="1" placeholder="例如: 50">
</div>
<div class="form-group">
<label>部門狀態</label>
<select id="deptStatus" name="deptStatus">
<option value="active">啟用中</option>
<option value="inactive">停用</option>
<option value="planning">規劃中</option>
</select>
</div>
</div>
<div class="form-group full-width">
<label>部門使命 (Mission)</label>
<textarea id="deptMission" name="deptMission" placeholder="• 請描述部門的核心使命..." rows="3"></textarea>
</div>
<div class="form-group full-width">
<label>部門願景 (Vision)</label>
<textarea id="deptVision" name="deptVision" placeholder="• 請描述部門的長期願景..." rows="3"></textarea>
</div>
<div class="form-group full-width">
<label>核心職責 (Core Functions) <span class="required">*</span></label>
<textarea id="deptCoreFunctions" name="deptCoreFunctions" required placeholder="• 職責一:...
• 職責二:...
• 職責三:..." rows="6"></textarea>
</div>
<div class="form-group full-width">
<label>關鍵績效指標 (KPIs)</label>
<textarea id="deptKPIs" name="deptKPIs" placeholder="• KPI 1...
• KPI 2...
• KPI 3..." rows="4"></textarea>
</div>
<div class="form-group full-width">
<label>協作部門</label>
<textarea id="deptCollaboration" name="deptCollaboration" placeholder="• 與XX部門協作進行...
• 與YY部門共同負責..." rows="3"></textarea>
</div>
<div class="form-group full-width">
<label>備注</label>
<textarea id="deptFunctionRemark" name="deptFunctionRemark" placeholder="請輸入其他補充說明..." rows="3"></textarea>
</div>
</div>
</form>
</div>
<div class="action-bar">
<div class="nav-buttons">
<button class="nav-btn" title="第一筆"><svg viewBox="0 0 24 24"><path d="M18.41 16.59L13.82 12l4.59-4.59L17 6l-6 6 6 6 1.41-1.41zM6 6h2v12H6V6z"/></svg></button>
<button class="nav-btn" title="上一筆"><svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg></button>
<button class="nav-btn" title="下一筆"><svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg></button>
<button class="nav-btn" title="最後一筆"><svg viewBox="0 0 24 24"><path d="M5.59 7.41L10.18 12l-4.59 4.59L7 18l6-6-6-6-1.41 1.41zM16 6h2v12h-2V6z"/></svg></button>
</div>
<div class="action-buttons">
<button class="btn btn-secondary" onclick="clearDeptFunctionForm()">清除</button>
<button class="btn btn-cancel" onclick="cancelDeptFunction()">取消</button>
<button class="btn btn-primary" onclick="saveDeptFunctionAndNew()">存檔續建</button>
<button class="btn btn-primary" onclick="saveDeptFunctionAndExit()">存檔離開</button>
</div>
</div>
</div>
<!-- ==================== 崗位描述模組 ==================== -->
<div class="module-content" id="module-jobdesc">
<header class="app-header desc-header">
<div class="icon">
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
</div>
<div>
<h1>崗位描述維護 (新增)</h1>
<div class="subtitle">Job Description Management</div>
</div>
</header>
<div class="form-card">
<div class="tabs">
<button class="tab-btn active" data-tab="jobdesc-basic">基礎資料</button>
</div>
<form id="jobDescForm">
<div class="tab-content active" id="tab-jobdesc-basic">
<button type="button" class="ai-generate-btn" onclick="generateJobDesc()">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<span>✨ I'm feeling lucky</span>
</button>
<!-- 頂部區域 -->
<div class="form-grid">
<div class="form-group">
<label>工號</label>
<div class="input-wrapper">
<input type="text" id="jd_empNo" name="empNo" placeholder="請輸入工號">
<button type="button" class="btn-icon" onclick="openEmpSearchModal()" title="選擇員工">
<svg viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
</button>
<button type="button" class="btn-icon" onclick="openOrgSearchModal()" title="選擇組織">
<svg viewBox="0 0 24 24"><path d="M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z"/></svg>
</button>
</div>
</div>
<div class="form-group">
<label>姓名</label>
<input type="text" id="jd_empName" name="empName" readonly placeholder="自動帶出">
</div>
<div class="form-group">
<label>崗位代碼</label>
<input type="text" id="jd_positionCode" name="positionCode" placeholder="請輸入崗位代碼">
</div>
<div class="form-group">
<label>版本更新日期</label>
<input type="date" id="jd_versionDate" name="versionDate">
</div>
</div>
<!-- 崗位基本信息 Section -->
<div class="section-box" style="margin-top: 24px;">
<div class="section-header green">崗位基本信息</div>
<div class="section-body">
<div class="form-grid">
<div class="form-group">
<label>崗位名稱</label>
<input type="text" id="jd_positionName" name="positionName" placeholder="請輸入崗位名稱">
</div>
<div class="form-group">
<label>所屬部門</label>
<input type="text" id="jd_department" name="department" placeholder="請輸入所屬部門">
</div>
<div class="form-group">
<label>部門職責</label>
<div class="input-wrapper">
<select id="jd_deptFunction" name="deptFunction" onchange="loadDeptFunctionInfo()">
<option value="">-- 請選擇部門職責 --</option>
</select>
<button type="button" class="btn-icon" onclick="refreshDeptFunctionList()" title="重新載入">
<svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
</button>
</div>
</div>
<div class="form-group">
<label>崗位生效日期</label>
<input type="date" id="jd_positionEffectiveDate" name="positionEffectiveDate">
</div>
<div class="form-group">
<label>直接領導職務</label>
<input type="text" id="jd_directSupervisor" name="directSupervisor" placeholder="請輸入直接領導職務">
</div>
<div class="form-group">
<label>崗位職等&職務</label>
<div class="input-wrapper">
<input type="text" id="jd_positionGradeJob" name="positionGradeJob" readonly placeholder="點擊選擇">
<button type="button" class="btn-icon" onclick="openGradeJobModal()" title="選擇職等職務">
<svg viewBox="0 0 24 24"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>
</button>
</div>
</div>
<div class="form-group">
<label>匯報對象職務</label>
<div class="input-wrapper">
<input type="text" id="jd_reportTo" name="reportTo" readonly placeholder="點擊選擇">
<button type="button" class="btn-icon" onclick="openReportToModal()" title="選擇匯報對象">
<svg viewBox="0 0 24 24"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>
</button>
</div>
</div>
<div class="form-group">
<label>直接下級(職位及人數)</label>
<input type="text" id="jd_directReports" name="directReports" placeholder="如:工程師 x 5人">
</div>
<div class="form-group">
<label>任職地點</label>
<select id="jd_workLocation" name="workLocation">
<option value="">請選擇</option>
<option value="HQ">總部</option>
<option value="TPE">台北辦公室</option>
<option value="TYC">桃園廠區</option>
<option value="KHH">高雄廠區</option>
<option value="SH">上海辦公室</option>
<option value="SZ">深圳辦公室</option>
</select>
</div>
<div class="form-group">
<label>員工屬性</label>
<select id="jd_empAttribute" name="empAttribute">
<option value="">請選擇</option>
<option value="FT">正式員工</option>
<option value="CT">約聘人員</option>
<option value="PT">兼職人員</option>
<option value="IN">實習生</option>
<option value="DP">派遣人員</option>
</select>
</div>
<div class="form-group"></div>
</div>
</div>
</div>
<!-- 部門職責資訊 Section (關聯顯示) -->
<div class="section-box" id="deptFunctionInfoSection" style="margin-top: 24px; display: none;">
<div class="section-header" style="background: linear-gradient(135deg, #8e44ad 0%, #9b59b6 100%);">部門職責資訊 (自動帶入)</div>
<div class="section-body">
<div class="form-grid">
<div class="form-group">
<label>部門職責編號</label>
<input type="text" id="jd_deptFunctionCode" readonly style="background: #f8f9fa;">
</div>
<div class="form-group">
<label>事業體</label>
<input type="text" id="jd_deptFunctionBU" readonly style="background: #f8f9fa;">
</div>
</div>
<div class="form-group full-width">
<label>部門使命</label>
<textarea id="jd_deptMission" readonly rows="2" style="background: #f8f9fa;"></textarea>
</div>
<div class="form-group full-width">
<label>部門核心職責</label>
<textarea id="jd_deptCoreFunctions" readonly rows="4" style="background: #f8f9fa;"></textarea>
</div>
<div class="form-group full-width">
<label>部門 KPIs</label>
<textarea id="jd_deptKPIs" readonly rows="3" style="background: #f8f9fa;"></textarea>
</div>
</div>
</div>
<!-- 崗位設置目的 -->
<div class="form-group full-width" style="margin-bottom: 24px;">
<label>崗位設置目的</label>
<div class="input-wrapper">
<input type="text" id="jd_positionPurpose" name="positionPurpose" placeholder="請輸入崗位設置目的" style="flex: 1;">
<button type="button" class="btn-icon" onclick="expandPurpose()" title="展開編輯">
<svg viewBox="0 0 24 24"><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
</button>
</div>
</div>
<!-- 職責描述 Section -->
<div class="section-box">
<div class="section-header green">職責描述</div>
<div class="section-body">
<div class="form-group full-width">
<label>主要崗位職責</label>
<textarea id="jd_mainResponsibilities" name="mainResponsibilities" class="numbered-textarea" rows="8" placeholder="1、
2、
3、
4、
5、">1、
2、
3、
4、</textarea>
</div>
</div>
</div>
<!-- 崗位要求 Section -->
<div class="section-box">
<div class="section-header green">崗位要求</div>
<div class="section-body">
<div class="form-grid">
<div class="form-group full-width">
<label>教育程度</label>
<input type="text" id="jd_education" name="education" placeholder="如:大學本科及以上學歷">
</div>
<div class="form-group full-width">
<label>基本技能</label>
<div class="input-wrapper">
<textarea id="jd_basicSkills" name="basicSkills" rows="2" placeholder="請輸入基本技能要求..." style="flex: 1;"></textarea>
<button type="button" class="btn-icon" style="align-self: flex-start;" onclick="expandField('basicSkills')" title="展開">
<svg viewBox="0 0 24 24"><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
</button>
</div>
</div>
<div class="form-group full-width">
<label>專業知識</label>
<div class="input-wrapper">
<textarea id="jd_professionalKnowledge" name="professionalKnowledge" rows="2" placeholder="請輸入專業知識要求..." style="flex: 1;"></textarea>
<button type="button" class="btn-icon" style="align-self: flex-start;" onclick="expandField('professionalKnowledge')" title="展開">
<svg viewBox="0 0 24 24"><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
</button>
</div>
</div>
<div class="form-group full-width">
<label>工作經驗</label>
<div class="input-wrapper">
<textarea id="jd_workExperienceReq" name="workExperienceReq" rows="2" placeholder="請輸入工作經驗要求..." style="flex: 1;"></textarea>
<button type="button" class="btn-icon" style="align-self: flex-start;" onclick="expandField('workExperienceReq')" title="展開">
<svg viewBox="0 0 24 24"><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
</button>
</div>
</div>
<div class="form-group full-width">
<label>其他</label>
<textarea id="jd_otherRequirements" name="otherRequirements" rows="3" placeholder="請輸入其他要求..."></textarea>
</div>
</div>
</div>
</div>
</div>
</form>
<div class="form-actions">
<div class="nav-buttons">
<button class="nav-btn" title="第一筆"><svg viewBox="0 0 24 24"><path d="M18.41 16.59L13.82 12l4.59-4.59L17 6l-6 6 6 6 1.41-1.41zM6 6h2v12H6V6z"/></svg></button>
<button class="nav-btn" title="上一筆"><svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg></button>
<button class="nav-btn" title="下一筆"><svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg></button>
<button class="nav-btn" title="最後一筆"><svg viewBox="0 0 24 24"><path d="M5.59 7.41L10.18 12l-4.59 4.59L7 18l6-6-6-6-1.41 1.41zM16 6h2v12h-2V6z"/></svg></button>
</div>
<div class="action-buttons">
<button type="button" class="btn btn-primary" onclick="saveJobDescAndExit()">
<svg viewBox="0 0 24 24"><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>
保存并退出(S)
</button>
<button type="button" class="btn btn-secondary" onclick="saveJobDescAndNew()">
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
保存并新增(N)
</button>
<button type="button" class="btn btn-cancel" onclick="cancelJobDescForm()">取消</button>
</div>
</div>
</div>
</div>
<!-- JSON Preview -->
<div class="data-preview">
<h4>📋 資料預覽 (JSON Format)</h4>
<pre id="jsonPreview">{}</pre>
</div>
</div>
<!-- Major Modal -->
<div class="modal-overlay" id="majorModal">
<div class="modal">
<div class="modal-header">
<h3>選擇專業要求</h3>
<button class="modal-close" onclick="closeMajorModal()"></button>
</div>
<div class="modal-body">
<div class="checkbox-group">
<div class="checkbox-item"><input type="checkbox" id="major_cs" value="資訊工程"><label for="major_cs">資訊工程</label></div>
<div class="checkbox-item"><input type="checkbox" id="major_ee" value="電機電子"><label for="major_ee">電機電子</label></div>
<div class="checkbox-item"><input type="checkbox" id="major_me" value="機械工程"><label for="major_me">機械工程</label></div>
<div class="checkbox-item"><input type="checkbox" id="major_ba" value="企業管理"><label for="major_ba">企業管理</label></div>
<div class="checkbox-item"><input type="checkbox" id="major_acc" value="會計財務"><label for="major_acc">會計財務</label></div>
<div class="checkbox-item"><input type="checkbox" id="major_hr" value="人力資源"><label for="major_hr">人力資源</label></div>
<div class="checkbox-item"><input type="checkbox" id="major_mkt" value="行銷企劃"><label for="major_mkt">行銷企劃</label></div>
<div class="checkbox-item"><input type="checkbox" id="major_law" value="法律"><label for="major_law">法律</label></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-cancel" onclick="closeMajorModal()">取消</button>
<button class="btn btn-primary" onclick="confirmMajor()">確認</button>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast">
<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 id="toastMessage">保存成功!</span>
</div>
<!-- ==================== 崗位清單模組 ==================== -->
<div class="module-content" id="module-positionlist">
<header class="app-header" style="background: linear-gradient(135deg, #8e44ad 0%, #9b59b6 100%);">
<div class="icon">
<svg viewBox="0 0 24 24" style="fill: white;"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg>
</div>
<div>
<h1 style="color: white;">崗位清單</h1>
<div class="subtitle" style="color: rgba(255,255,255,0.8);">Position List with Sorting</div>
</div>
</header>
<div class="form-card">
<div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
<div>
<button type="button" class="btn btn-primary" onclick="loadPositionList()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
載入清單
</button>
<button type="button" class="btn btn-secondary" onclick="exportPositionListCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
匯出 CSV
</button>
</div>
<div style="color: var(--text-secondary);">
點擊欄位標題進行排序
</div>
</div>
<div style="overflow-x: auto;">
<table id="positionListTable" style="width: 100%; border-collapse: collapse; background: white;">
<thead>
<tr style="background: var(--primary); color: white;">
<th class="sortable" data-sort="positionCode" onclick="sortPositionList('positionCode')" style="padding: 12px; cursor: pointer; text-align: left;">
崗位編號 <span class="sort-icon"></span>
</th>
<th class="sortable" data-sort="positionName" onclick="sortPositionList('positionName')" style="padding: 12px; cursor: pointer; text-align: left;">
崗位名稱 <span class="sort-icon"></span>
</th>
<th class="sortable" data-sort="positionCategory" onclick="sortPositionList('positionCategory')" style="padding: 12px; cursor: pointer; text-align: left;">
崗位類別 <span class="sort-icon"></span>
</th>
<th class="sortable" data-sort="positionNature" onclick="sortPositionList('positionNature')" style="padding: 12px; cursor: pointer; text-align: left;">
崗位性質 <span class="sort-icon"></span>
</th>
<th class="sortable" data-sort="headcount" onclick="sortPositionList('headcount')" style="padding: 12px; cursor: pointer; text-align: left;">
編制人數 <span class="sort-icon"></span>
</th>
<th class="sortable" data-sort="positionLevel" onclick="sortPositionList('positionLevel')" style="padding: 12px; cursor: pointer; text-align: left;">
崗位等級 <span class="sort-icon"></span>
</th>
<th class="sortable" data-sort="effectiveDate" onclick="sortPositionList('effectiveDate')" style="padding: 12px; cursor: pointer; text-align: left;">
生效日期 <span class="sort-icon"></span>
</th>
<th style="padding: 12px; text-align: center;">操作</th>
</tr>
</thead>
<tbody id="positionListBody">
<tr>
<td colspan="8" style="padding: 40px; text-align: center; color: var(--text-secondary);">
點擊「載入清單」按鈕以顯示崗位資料
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ==================== 管理者頁面模組 ==================== -->
<div class="module-content" id="module-admin">
<header class="app-header" style="background: linear-gradient(135deg, #c0392b 0%, #e74c3c 100%);">
<div class="icon">
<svg viewBox="0 0 24 24" style="fill: white;"><path d="M19.14,12.94c0.04-0.31,0.06-0.63,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>
</div>
<div>
<h1 style="color: white;">管理者頁面</h1>
<div class="subtitle" style="color: rgba(255,255,255,0.8);">User Administration</div>
</div>
</header>
<div class="form-card">
<div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
<h2 style="color: var(--primary); margin: 0;">使用者清單</h2>
<div style="display: flex; gap: 10px;">
<button type="button" class="btn btn-primary" onclick="showAddUserModal()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
新增使用者
</button>
<button type="button" class="btn btn-secondary" onclick="exportUsersCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
匯出 CSV
</button>
</div>
</div>
<div style="overflow-x: auto;">
<table id="userListTable" style="width: 100%; border-collapse: collapse; background: white;">
<thead>
<tr style="background: #c0392b; color: white;">
<th style="padding: 12px; text-align: left;">工號</th>
<th style="padding: 12px; text-align: left;">使用者姓名</th>
<th style="padding: 12px; text-align: left;">Email 信箱</th>
<th style="padding: 12px; text-align: left;">權限等級</th>
<th style="padding: 12px; text-align: left;">建立日期</th>
<th style="padding: 12px; text-align: center;">操作</th>
</tr>
</thead>
<tbody id="userListBody">
<tr>
<td style="padding: 12px; border-bottom: 1px solid #eee;">A001</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">系統管理員</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">admin@company.com</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">
<span style="background: #e74c3c; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">最高權限管理者</span>
</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">2024-01-01</td>
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="editUser('A001')">編輯</button>
</td>
</tr>
<tr>
<td style="padding: 12px; border-bottom: 1px solid #eee;">A002</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">人資主管</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">hr_manager@company.com</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">
<span style="background: #f39c12; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">管理者</span>
</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">2024-01-15</td>
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="editUser('A002')">編輯</button>
<button class="btn btn-cancel" style="padding: 4px 8px; font-size: 12px;" onclick="deleteUser('A002')">刪除</button>
</td>
</tr>
<tr>
<td style="padding: 12px; border-bottom: 1px solid #eee;">A003</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">一般員工</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">employee@company.com</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">
<span style="background: #27ae60; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">一般使用者</span>
</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">2024-02-01</td>
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="editUser('A003')">編輯</button>
<button class="btn btn-cancel" style="padding: 4px 8px; font-size: 12px;" onclick="deleteUser('A003')">刪除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 崗位資料管理 -->
<div class="form-card" style="margin-top: 30px;">
<div style="margin-bottom: 20px;">
<h2 style="color: var(--primary); margin: 0 0 10px 0;">崗位資料管理</h2>
<p style="color: var(--text-secondary); font-size: 14px; margin: 0;">管理和匯出完整的崗位資料表</p>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px;">
<!-- 匯出完整崗位資料 -->
<div style="border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; background: #f8f9fa;">
<div style="display: flex; align-items: center; margin-bottom: 15px;">
<svg viewBox="0 0 24 24" style="width: 24px; height: 24px; fill: var(--primary); margin-right: 10px;">
<path d="M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2z"/>
</svg>
<h3 style="margin: 0; font-size: 16px; color: var(--text-primary);">匯出完整崗位資料</h3>
</div>
<p style="font-size: 14px; color: var(--text-secondary); margin-bottom: 15px;">
匯出所有崗位的完整資料,包含基本資料、描述、要求等所有欄位。
</p>
<button type="button" class="btn btn-primary" style="width: 100%;" onclick="exportCompletePositionData()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;">
<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/>
</svg>
匯出完整 CSV
</button>
</div>
<!-- 統計資訊 -->
<div style="border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; background: #f8f9fa;">
<div style="display: flex; align-items: center; margin-bottom: 15px;">
<svg viewBox="0 0 24 24" style="width: 24px; height: 24px; fill: var(--success); margin-right: 10px;">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"/>
</svg>
<h3 style="margin: 0; font-size: 16px; color: var(--text-primary);">資料統計</h3>
</div>
<div style="font-size: 14px; color: var(--text-secondary);">
<p style="margin: 5px 0;">崗位總數: <strong id="totalPositionsCount">-</strong></p>
<p style="margin: 5px 0;">已描述: <strong id="describedPositionsCount">-</strong></p>
<p style="margin: 5px 0;">未描述: <strong id="undescribedPositionsCount">-</strong></p>
</div>
<button type="button" class="btn btn-secondary" style="width: 100%; margin-top: 10px;" onclick="refreshPositionStats()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;">
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
</svg>
更新統計
</button>
</div>
</div>
</div>
</div>
<!-- 新增/編輯使用者彈窗 -->
<div id="userModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center;">
<div style="background: white; border-radius: 8px; padding: 30px; max-width: 500px; width: 90%;">
<h3 id="userModalTitle" style="margin: 0 0 20px 0; color: var(--primary);">新增使用者</h3>
<form id="userForm">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500;">工號 <span style="color: red;">*</span></label>
<input type="text" id="userEmployeeId" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500;">使用者姓名 <span style="color: red;">*</span></label>
<input type="text" id="userName" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500;">Email 信箱 <span style="color: red;">*</span></label>
<input type="email" id="userEmail" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500;">權限等級 <span style="color: red;">*</span></label>
<select id="userRole" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
<option value="">請選擇</option>
<option value="user">一般使用者</option>
<option value="admin">管理者</option>
<option value="superadmin">最高權限管理者</option>
</select>
</div>
<div style="display: flex; justify-content: flex-end; gap: 10px;">
<button type="button" class="btn btn-cancel" onclick="closeUserModal()">取消</button>
<button type="submit" class="btn btn-primary" onclick="saveUser(event)">儲存</button>
</div>
</form>
</div>
</div>
<script>
// ==================== XSS 防護工具函數 ====================
/**
* 消毒 HTML 字串,防止 XSS 攻擊
* @param {string} str - 需要消毒的字串
* @returns {string} - 安全的字串
*/
function sanitizeHTML(str) {
if (str === null || str === undefined) return '';
const temp = document.createElement('div');
temp.textContent = str;
return temp.innerHTML;
}
/**
* 安全設定元素文字內容
* @param {HTMLElement} element - 目標元素
* @param {string} text - 文字內容
*/
function safeSetText(element, text) {
if (element) {
element.textContent = text;
}
}
// ==================== AI Generation Functions ====================
async function callClaudeAPI(prompt, api = 'gemini') {
try {
// 調用後端 Flask API避免 CORS 錯誤
const response = await fetch("/api/llm/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
api: api,
prompt: prompt,
max_tokens: 2000
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `API 請求失敗: ${response.status}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'API 調用失敗');
}
let responseText = data.text;
responseText = responseText.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
return JSON.parse(responseText);
} catch (error) {
console.error("Error calling LLM API:", error);
// 嘗試解析更詳細的錯誤訊息
let errorDetails = error.message;
try {
// 如果錯誤訊息是 JSON 格式,嘗試美化顯示
const errorJson = JSON.parse(error.message);
errorDetails = JSON.stringify(errorJson, null, 2);
} catch (e) {
// 不是 JSON使用原始訊息
}
// 創建可複製的錯誤對話框
showCopyableError({
title: 'AI 生成錯誤',
message: error.message,
details: errorDetails,
suggestions: [
'Flask 後端已啟動 (python start_server.py)',
'已在 .env 文件中配置有效的 LLM API Key',
'網路連線正常',
'嘗試使用不同的 LLM API (DeepSeek 或 OpenAI)'
]
});
throw error;
}
}
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>';
}
}
// Helper: 只在欄位為空時填入值
function fillIfEmpty(elementId, value) {
const el = document.getElementById(elementId);
if (el && !el.value.trim() && value) {
el.value = value;
return true;
}
return false;
}
// Helper: 獲取欄位當前值
function getFieldValue(elementId) {
const el = document.getElementById(elementId);
return el ? el.value.trim() : '';
}
// Helper: 獲取空白欄位列表
function getEmptyFields(fieldIds) {
return fieldIds.filter(id => !getFieldValue(id));
}
async function generatePositionBasic() {
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) {
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 = `請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
${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: 崗位描述2-3句話描述工作內容
- 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++;
updateCategoryName();
}
if (fillIfEmpty('positionNature', data.positionNature)) {
filledCount++;
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++;
updatePreview();
showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
} catch (error) {
showToast('生成失敗,請稍後再試');
} finally {
setButtonLoading(btn, false);
}
}
async function generatePositionRecruit() {
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) {
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 = `請為HR崗位管理系統生成「${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++;
updatePreview();
showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
} catch (error) {
showToast('生成失敗,請稍後再試');
} finally {
setButtonLoading(btn, false);
}
}
async function generateJobBasic() {
const btn = event.target.closest('.ai-generate-btn');
const allFields = ['jobCategoryCode', 'jobCode', 'jobName', 'jobNameEn', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel'];
const emptyFields = getEmptyFields(allFields);
// 也檢查 checkbox 是否需要設定
const needCheckboxes = !document.getElementById('hasAttendanceBonus').checked && !document.getElementById('hasHousingAllowance').checked;
if (emptyFields.length === 0 && !needCheckboxes) {
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 = `請為HR職務管理系統生成職務基礎資料。請用繁體中文回覆。
${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++;
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++;
// Checkbox 只在尚未勾選時設定
if (needCheckboxes) {
const attendanceCheckbox = document.getElementById('hasAttendanceBonus');
const housingCheckbox = document.getElementById('hasHousingAllowance');
if (data.hasAttendanceBonus === true) {
attendanceCheckbox.checked = true;
document.getElementById('attendanceLabel').textContent = '是';
filledCount++;
}
if (data.hasHousingAllowance === true) {
housingCheckbox.checked = true;
document.getElementById('housingLabel').textContent = '是';
filledCount++;
}
}
updatePreview();
showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
} catch (error) {
showToast('生成失敗,請稍後再試');
} finally {
setButtonLoading(btn, false);
}
}
async function generateJobDesc() {
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() : '';
// 對於 mainResponsibilities檢查是否只有預設的編號
if (id === 'jd_mainResponsibilities') {
return !value || value === '1、\n2、\n3、\n4、' || value === '1、\n2、\n3、\n4、\n5、';
}
return !value;
});
if (emptyFields.length === 0) {
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 = `請為HR崗位描述管理系統生成崗位描述資料。請用繁體中文回覆。
${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;
// 使用映射來處理 jd_ 前綴
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() : '';
// 特殊處理 mainResponsibilities
const isEmpty = !currentValue || currentValue === '1、\n2、\n3、\n4、';
if (isEmpty) {
el.value = data[apiField];
filledCount++;
}
}
});
updatePreview();
showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
} catch (error) {
showToast('生成失敗,請稍後再試');
} finally {
setButtonLoading(btn, false);
}
}
// ==================== Module Switching ====================
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();
}
document.querySelectorAll('.module-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.module-btn').forEach(b => {
b.classList.remove('active', 'job-active', 'desc-active');
});
document.querySelectorAll('.module-content').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
if (btn.dataset.module === 'job') btn.classList.add('job-active');
if (btn.dataset.module === 'jobdesc') btn.classList.add('desc-active');
document.getElementById('module-' + btn.dataset.module).classList.add('active');
// 自動刷新崗位清單
if (btn.dataset.module === 'positionlist') {
loadPositionList();
}
updatePreview();
});
});
// ==================== Tab Switching ====================
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
const parent = btn.closest('.form-card');
parent.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
parent.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
});
});
// ==================== Position Form Logic ====================
const categoryMap = { '01': '技術職', '02': '管理職', '03': '業務職', '04': '行政職' };
const natureMap = { 'FT': '全職', 'PT': '兼職', 'CT': '約聘', 'IN': '實習' };
function updateCategoryName() {
const category = document.getElementById('positionCategory').value;
document.getElementById('positionCategoryName').value = categoryMap[category] || '';
updatePreview();
}
function updateNatureName() {
const nature = document.getElementById('positionNature').value;
document.getElementById('positionNatureName').value = natureMap[nature] || '';
updatePreview();
}
function changePositionCode() {
const currentCode = document.getElementById('positionCode').value;
const newCode = prompt('請輸入新的崗位編號:', currentCode);
if (newCode && newCode !== currentCode) {
document.getElementById('positionCode').value = newCode;
showToast('崗位編號已更改!');
updatePreview();
}
}
// ==================== Job Form Logic ====================
const jobCategoryMap = { 'MGR': '管理職', 'TECH': '技術職', 'SALE': '業務職', 'ADMIN': '行政職', 'RD': '研發職', 'PROD': '生產職' };
function updateJobCategoryName() {
const category = document.getElementById('jobCategoryCode').value;
document.getElementById('jobCategoryName').value = jobCategoryMap[category] || '';
updatePreview();
}
function changeJobCode() {
const currentCode = document.getElementById('jobCode').value;
const newCode = prompt('請輸入新的職務編號:', currentCode);
if (newCode && newCode !== currentCode) {
document.getElementById('jobCode').value = newCode;
showToast('職務編號已更改!');
updatePreview();
}
}
document.getElementById('hasAttendanceBonus').addEventListener('change', function() {
document.getElementById('attendanceLabel').textContent = this.checked ? '是' : '否';
updatePreview();
});
document.getElementById('hasHousingAllowance').addEventListener('change', function() {
document.getElementById('housingLabel').textContent = this.checked ? '是' : '否';
updatePreview();
});
// ==================== Job Description Logic ====================
function openEmpSearchModal() { showToast('員工選擇功能 (待整合)'); }
function openOrgSearchModal() { showToast('組織選擇功能 (待整合)'); }
function openGradeJobModal() { showToast('職等職務選擇功能 (待整合)'); }
function openReportToModal() { showToast('匯報對象選擇功能 (待整合)'); }
function expandPurpose() { showToast('展開編輯功能 (待開發)'); }
function expandField(fieldName) { showToast('展開編輯功能 (待開發)'); }
// ==================== Major Modal ====================
function openMajorModal() { document.getElementById('majorModal').classList.add('show'); }
function closeMajorModal() { document.getElementById('majorModal').classList.remove('show'); }
function confirmMajor() {
const selected = [];
document.querySelectorAll('#majorModal input[type="checkbox"]:checked').forEach(cb => {
selected.push(cb.value);
});
document.getElementById('majorReq').value = selected.join(', ');
closeMajorModal();
updatePreview();
}
// ==================== Data Handling ====================
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;
}
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('hasAttendanceBonus').checked;
data.hasHousingAllowance = document.getElementById('hasHousingAllowance').checked;
return data;
}
function getJobDescFormData() {
const form = document.getElementById('jobDescForm');
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;
});
// 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;
});
// Purpose & Responsibilities
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;
// Requirements
['education', 'basicSkills', 'professionalKnowledge', 'workExperienceReq', 'otherRequirements'].forEach(field => {
const el = document.getElementById('jd_' + field);
if (el && el.value) data.requirements[field] = el.value;
});
return data;
}
function updatePreview() {
const activeModule = document.querySelector('.module-btn.active').dataset.module;
let data;
if (activeModule === 'position') {
data = { module: '崗位基礎資料', ...getPositionFormData() };
} else if (activeModule === 'job') {
data = { module: '職務基礎資料', ...getJobFormData() };
} else {
data = { module: '崗位描述', ...getJobDescFormData() };
}
document.getElementById('jsonPreview').textContent = JSON.stringify(data, null, 2);
}
function showToast(message) {
const toast = document.getElementById('toast');
document.getElementById('toastMessage').textContent = message;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3000);
}
// ==================== Form Actions ====================
function savePositionAndExit() {
const form = document.getElementById('positionForm');
if (!form.checkValidity()) { form.reportValidity(); return; }
console.log('Save Position:', getPositionFormData());
showToast('崗位資料已保存!');
}
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();
}
function cancelPositionForm() {
if (confirm('確定要取消嗎?未保存的資料將會遺失。')) {
document.getElementById('positionForm').reset();
updatePreview();
}
}
function saveJobAndExit() {
const form = document.getElementById('jobForm');
if (!form.checkValidity()) { form.reportValidity(); return; }
console.log('Save Job:', getJobFormData());
showToast('職務資料已保存!');
}
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();
}
function cancelJobForm() {
if (confirm('確定要取消嗎?未保存的資料將會遺失。')) {
document.getElementById('jobForm').reset();
document.getElementById('attendanceLabel').textContent = '否';
document.getElementById('housingLabel').textContent = '否';
updatePreview();
}
}
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);
}
}
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);
}
}
function cancelJobDescForm() {
if (confirm('確定要取消嗎?未保存的資料將會遺失。')) {
document.getElementById('jobDescForm').reset();
document.getElementById('jd_mainResponsibilities').value = '1、\n2、\n3、\n4、';
updatePreview();
}
}
// ==================== Event Listeners ====================
document.querySelectorAll('input, select, textarea').forEach(el => {
el.addEventListener('input', updatePreview);
el.addEventListener('change', updatePreview);
});
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
const activeModule = document.querySelector('.module-btn.active').dataset.module;
if (e.key === 's') {
e.preventDefault();
if (activeModule === 'position') savePositionAndExit();
else if (activeModule === 'job') saveJobAndExit();
else saveJobDescAndExit();
} else if (e.key === 'n') {
e.preventDefault();
if (activeModule === 'position') savePositionAndNew();
else if (activeModule === 'job') saveJobAndNew();
else saveJobDescAndNew();
}
}
});
updatePreview();
// 顯示可複製的錯誤訊息
function showCopyableError(options) {
const { title, message, details, suggestions } = options;
// 創建對話框
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: fadeIn 0.3s;
`;
modal.innerHTML = `
<div style="
background: white;
border-radius: 12px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
">
<!-- Header -->
<div style="
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
padding: 20px;
display: flex;
align-items: center;
gap: 15px;
">
<span style="font-size: 2rem;">❌</span>
<h3 style="margin: 0; font-size: 1.3rem; flex: 1;">${sanitizeHTML(title)}</h3>
<button onclick="closeErrorModal(this)" style="
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
">×</button>
</div>
<!-- Body -->
<div style="
padding: 25px;
overflow-y: auto;
flex: 1;
">
<div style="
color: #333;
line-height: 1.6;
margin-bottom: 20px;
font-size: 1rem;
">${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;
user-select: none;
margin-bottom: 10px;
">🔍 詳細錯誤訊息(點擊展開)</summary>
<pre id="errorDetailsText" style="
background: white;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-size: 0.85rem;
color: #666;
margin: 10px 0 0 0;
white-space: pre-wrap;
word-break: break-word;
max-height: 300px;
overflow-y: auto;
">${sanitizeHTML(details)}</pre>
<button onclick="copyErrorDetails()" style="
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
font-size: 0.9rem;
">📋 複製錯誤訊息</button>
</details>
` : ''}
</div>
<!-- Footer -->
<div style="
padding: 15px 25px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
gap: 10px;
">
<button onclick="closeErrorModal(this)" style="
background: #007bff;
color: white;
border: none;
padding: 10px 25px;
border-radius: 6px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
">確定</button>
</div>
</div>
`;
document.body.appendChild(modal);
// 點擊背景關閉
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
}
// 關閉錯誤訊息對話框
function closeErrorModal(button) {
const modal = button.closest('[style*="position: fixed"]');
if (modal) {
modal.style.opacity = '0';
setTimeout(() => modal.remove(), 300);
}
}
// 複製錯誤訊息到剪貼板
function copyErrorDetails() {
const text = document.getElementById('errorDetailsText').textContent;
navigator.clipboard.writeText(text).then(() => {
alert('錯誤訊息已複製到剪貼板!');
}).catch(err => {
// Fallback: 選取文字
const range = document.createRange();
range.selectNode(document.getElementById('errorDetailsText'));
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
try {
document.execCommand('copy');
alert('錯誤訊息已複製到剪貼板!');
} catch (e) {
alert('複製失敗,請手動選取並複製');
}
});
}
// ==================== CSV 匯入匯出函數 ====================
// 崗位資料 CSV 範本下載
function downloadPositionCSVTemplate() {
window.location.href = API_BASE_URL + '/positions/csv-template';
showToast('正在下載崗位資料範本...');
}
// 職務資料 CSV 範本下載
function downloadJobCSVTemplate() {
window.location.href = API_BASE_URL + '/jobs/csv-template';
showToast('正在下載職務資料範本...');
}
// 崗位資料 CSV 匯出
function exportPositionsCSV() {
// 收集所有崗位資料(這裡簡化為當前表單資料)
const data = [{
positionCode: getFieldValue('positionCode'),
positionName: getFieldValue('positionName'),
positionCategory: getFieldValue('positionCategory'),
positionNature: getFieldValue('positionNature'),
headcount: getFieldValue('headcount'),
positionLevel: getFieldValue('positionLevel'),
effectiveDate: getFieldValue('effectiveDate'),
positionDesc: getFieldValue('positionDesc'),
positionRemark: getFieldValue('positionRemark'),
minEducation: getFieldValue('minEducation'),
salaryRange: getFieldValue('salaryRange'),
workExperience: getFieldValue('workExperience'),
minAge: getFieldValue('minAge'),
maxAge: getFieldValue('maxAge')
}];
const headers = ['positionCode', 'positionName', 'positionCategory', 'positionNature',
'headcount', 'positionLevel', 'effectiveDate', 'positionDesc', 'positionRemark',
'minEducation', 'salaryRange', 'workExperience', 'minAge', 'maxAge'];
CSVUtils.exportToCSV(data, 'positions.csv', headers);
showToast('崗位資料已匯出!');
}
// 崗位資料 CSV 匯入觸發
function importPositionsCSV() {
document.getElementById('positionCSVInput').click();
}
// 處理崗位 CSV 匯入
function handlePositionCSVImport(event) {
const file = event.target.files[0];
if (!file) return;
// 使用 FormData 上傳 CSV 檔案
const formData = new FormData();
formData.append('file', file);
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);
// 重新載入崗位列表
// loadPositions();
} else {
alert('匯入失敗: ' + (data.error || '未知錯誤'));
}
})
.catch(error => {
console.error('匯入錯誤:', error);
alert('匯入失敗: ' + error.message);
})
.finally(() => {
// 重置 input
event.target.value = '';
});
}
// 職務資料 CSV 匯出
function exportJobsCSV() {
const data = [{
jobCategoryCode: getFieldValue('jobCategoryCode'),
jobCategoryName: getFieldValue('jobCategoryName'),
jobCode: getFieldValue('jobCode'),
jobName: getFieldValue('jobName'),
jobNameEn: getFieldValue('jobNameEn'),
jobEffectiveDate: getFieldValue('jobEffectiveDate'),
jobHeadcount: getFieldValue('jobHeadcount'),
jobSortOrder: getFieldValue('jobSortOrder'),
jobRemark: getFieldValue('jobRemark'),
jobLevel: getFieldValue('jobLevel'),
hasAttendanceBonus: document.getElementById('hasAttendanceBonus')?.checked,
hasHousingAllowance: document.getElementById('hasHousingAllowance')?.checked
}];
const headers = ['jobCategoryCode', 'jobCategoryName', 'jobCode', 'jobName', 'jobNameEn',
'jobEffectiveDate', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel',
'hasAttendanceBonus', 'hasHousingAllowance'];
CSVUtils.exportToCSV(data, 'jobs.csv', headers);
showToast('職務資料已匯出!');
}
// 職務資料 CSV 匯入觸發
function importJobsCSV() {
document.getElementById('jobCSVInput').click();
}
// 處理職務 CSV 匯入
function handleJobCSVImport(event) {
const file = event.target.files[0];
if (!file) return;
// 使用 FormData 上傳 CSV 檔案
const formData = new FormData();
formData.append('file', file);
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);
// 重新載入職務列表
// loadJobs();
} else {
alert('匯入失敗: ' + (data.error || '未知錯誤'));
}
})
.catch(error => {
console.error('匯入錯誤:', error);
alert('匯入失敗: ' + error.message);
})
.finally(() => {
// 重置 input
event.target.value = '';
});
}
// 崗位描述 CSV 匯出
function exportDescriptionsCSV() {
const data = [{
descPositionCode: getFieldValue('descPositionCode'),
descPositionName: getFieldValue('descPositionName'),
descEffectiveDate: getFieldValue('descEffectiveDate'),
jobDuties: getFieldValue('jobDuties'),
requiredSkills: getFieldValue('requiredSkills'),
workEnvironment: getFieldValue('workEnvironment'),
careerPath: getFieldValue('careerPath'),
descRemark: getFieldValue('descRemark')
}];
const headers = ['descPositionCode', 'descPositionName', 'descEffectiveDate', 'jobDuties',
'requiredSkills', 'workEnvironment', 'careerPath', 'descRemark'];
CSVUtils.exportToCSV(data, 'job_descriptions.csv', headers);
showToast('崗位描述已匯出!');
}
// 崗位描述 CSV 匯入觸發
function importDescriptionsCSV() {
document.getElementById('descCSVInput').click();
}
// 處理崗位描述 CSV 匯入
function handleDescCSVImport(event) {
const file = event.target.files[0];
if (!file) return;
CSVUtils.importFromCSV(file, (data) => {
if (data && data.length > 0) {
const firstRow = data[0];
Object.keys(firstRow).forEach(key => {
const element = document.getElementById(key);
if (element) {
element.value = firstRow[key];
}
});
showToast(`已匯入 ${data.length} 筆崗位描述資料(顯示第一筆)`);
}
});
event.target.value = '';
}
// ==================== 崗位清單功能 ====================
let positionListData = [];
let currentSortColumn = '';
let currentSortDirection = 'asc';
// 載入崗位清單(示範資料)
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();
}
}
// 渲染崗位清單
function renderPositionList() {
const tbody = document.getElementById('positionListBody');
if (positionListData.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" style="padding: 40px; text-align: center; color: var(--text-secondary);">沗有資料,請先建立崗位或點擊「載入清單」</td></tr>';
return;
}
tbody.innerHTML = positionListData.map(item => `
<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 12px;">${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('');
}
// 排序崗位清單
function sortPositionList(column) {
if (currentSortColumn === column) {
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
} else {
currentSortColumn = column;
currentSortDirection = 'asc';
}
positionListData.sort((a, b) => {
let valA = a[column];
let valB = b[column];
if (typeof valA === 'string') {
valA = valA.toLowerCase();
valB = valB.toLowerCase();
}
if (valA < valB) return currentSortDirection === 'asc' ? -1 : 1;
if (valA > valB) return currentSortDirection === 'asc' ? 1 : -1;
return 0;
});
// 更新排序圖示
document.querySelectorAll('.sort-icon').forEach(icon => icon.textContent = '');
const currentHeader = document.querySelector(`th[data-sort="${column}"] .sort-icon`);
if (currentHeader) {
currentHeader.textContent = currentSortDirection === 'asc' ? ' ^' : ' v';
}
renderPositionList();
}
// 檢視崗位 - 切換到崗位基礎資料頁籤並載入資料
function viewPosition(code) {
const position = positionListData.find(p => p.positionCode === code);
if (position) {
// 切換到崗位基礎資料模組
document.querySelectorAll('.module-btn').forEach(b => {
b.classList.remove('active', 'job-active', 'desc-active');
});
document.querySelector('.module-btn[data-module="position"]').classList.add('active');
document.querySelectorAll('.module-content').forEach(m => m.classList.remove('active'));
document.getElementById('module-position').classList.add('active');
// 填入崗位資料
document.getElementById('positionCode').value = position.positionCode || '';
document.getElementById('positionName').value = position.positionName || '';
// 根據崗位類別設定下拉選單
const categoryMap = {'技術職': '01', '管理職': '02', '業務職': '03', '行政職': '04', '專業職': '05'};
const categoryCode = categoryMap[position.positionCategory] || '';
document.getElementById('positionCategory').value = categoryCode;
if (typeof updateCategoryName === 'function') updateCategoryName();
document.getElementById('headcount').value = position.headcount || '';
document.getElementById('effectiveDate').value = position.effectiveDate || '';
// 填入組織欄位
if (document.getElementById('businessUnit')) {
document.getElementById('businessUnit').value = position.businessUnit || '';
}
if (document.getElementById('department')) {
document.getElementById('department').value = position.department || '';
}
showToast('已載入崗位: ' + position.positionName);
}
}
// 檢視崗位描述
async function viewPositionDesc(positionCode) {
try {
// 獲取崗位描述資料
const response = await fetch(API_BASE_URL + '/position-descriptions/' + positionCode);
const result = await response.json();
if (result.success && result.data) {
const desc = result.data;
// 切換到崗位描述模組
switchModule('jobdesc');
// 填入基本資訊
if (document.getElementById('jobDescPositionCode')) {
document.getElementById('jobDescPositionCode').value = desc.positionCode || '';
}
if (document.getElementById('jobDescPositionName')) {
document.getElementById('jobDescPositionName').value = desc.positionName || '';
}
if (document.getElementById('jobDescEffectiveDate')) {
document.getElementById('jobDescEffectiveDate').value = desc.effectiveDate || '';
}
// 填入工作職責
if (document.getElementById('jobDescMainResponsibilities')) {
document.getElementById('jobDescMainResponsibilities').value = desc.jobDuties || '';
}
// 填入任職要求
if (document.getElementById('jobDescBasicSkills')) {
document.getElementById('jobDescBasicSkills').value = desc.requiredSkills || '';
}
// 填入工作環境
if (document.getElementById('jobDescWorkLocation')) {
document.getElementById('jobDescWorkLocation').value = desc.workEnvironment || '';
}
// 填入職涯發展
if (document.getElementById('jobDescCareerPath')) {
document.getElementById('jobDescCareerPath').value = desc.careerPath || '';
}
showToast('已載入崗位描述: ' + desc.positionName);
} else {
// 如果沒有描述資料,只載入基本資訊
const position = positionListData.find(p => p.positionCode === positionCode);
if (position) {
switchModule('jobdesc');
if (document.getElementById('jobDescPositionCode')) {
document.getElementById('jobDescPositionCode').value = position.positionCode || '';
}
if (document.getElementById('jobDescPositionName')) {
document.getElementById('jobDescPositionName').value = position.positionName || '';
}
if (document.getElementById('jobDescEffectiveDate')) {
document.getElementById('jobDescEffectiveDate').value = position.effectiveDate || '';
}
showToast('該崗位尚無描述資料,您可以新增描述');
} else {
alert('找不到崗位資料');
}
}
} catch (error) {
console.error('載入崗位描述錯誤:', error);
alert('載入崗位描述失敗: ' + error.message);
}
}
// 匯出崗位清單 CSV
function exportPositionListCSV() {
if (positionListData.length === 0) {
showToast('請先載入清單資料');
return;
}
const headers = ['positionCode', 'positionName', 'businessUnit', 'department', 'positionCategory', 'headcount', 'effectiveDate'];
CSVUtils.exportToCSV(positionListData, 'position_list.csv', headers);
showToast('崗位清單已匯出!');
}
// ==================== 部門職責模組功能 ====================
let deptFunctionData = [
{
deptFunctionCode: 'DF-001',
deptFunctionName: '軟體研發部職責',
deptFunctionBU: 'ITBU',
deptFunctionDept: '軟體研發部',
deptManager: '研發部經理',
deptFunctionEffectiveDate: '2024-01-01',
deptHeadcount: 30,
deptStatus: 'active',
deptMission: '• 開發高品質軟體產品\n• 持續創新技術解決方案',
deptVision: '• 成為業界領先的軟體研發團隊',
deptCoreFunctions: '• 軟體系統設計與開發\n• 程式碼品質管理\n• 技術架構規劃\n• 新技術研究與導入',
deptKPIs: '• 專案準時交付率 > 90%\n• 程式碼缺陷率 < 1%\n• 客戶滿意度 > 4.5/5',
deptCollaboration: '• 與產品部協作需求分析\n• 與品保部協作測試驗證',
deptFunctionRemark: ''
},
{
deptFunctionCode: 'DF-002',
deptFunctionName: '人力資源部職責',
deptFunctionBU: 'HRBU',
deptFunctionDept: '人力資源部',
deptManager: '人資部經理',
deptFunctionEffectiveDate: '2024-01-01',
deptHeadcount: 15,
deptStatus: 'active',
deptMission: '• 吸引並留住優秀人才\n• 建立高效能組織文化',
deptVision: '• 成為最佳雇主品牌的推手',
deptCoreFunctions: '• 人才招募與甄選\n• 員工培訓與發展\n• 薪酬福利管理\n• 員工關係維護',
deptKPIs: '• 人才留任率 > 85%\n• 招募周期 < 45天\n• 培訓滿意度 > 4.0/5',
deptCollaboration: '• 與各部門協作人力規劃\n• 與財務部協作薪酬預算',
deptFunctionRemark: ''
}
];
function generateDeptFunction() {
const btn = event.target.closest('.ai-generate-btn');
const allFields = ['deptFunctionCode', 'deptFunctionName', 'deptFunctionBU', 'deptFunctionDept', 'deptManager', 'deptMission', 'deptVision', 'deptCoreFunctions', 'deptKPIs'];
const emptyFields = getEmptyFields(allFields);
if (emptyFields.length === 0) {
showToast('所有欄位都已填寫完成!');
return;
}
setButtonLoading(btn, true);
const existingData = {};
allFields.forEach(field => {
const value = getFieldValue(field);
if (value) existingData[field] = value;
});
const contextInfo = Object.keys(existingData).length > 0
? `\n\n已填寫的資料(請參考這些內容來生成相關的資料):\n${JSON.stringify(existingData, null, 2)}`
: '';
const prompt = `請為HR部門職責管理系統生成部門職責資料。請用繁體中文回覆。
${contextInfo}
請「只生成」以下這些尚未填寫的欄位:${emptyFields.join(', ')}
欄位說明:
- deptFunctionCode: 部門職責編號(格式如 DF-001, DF-002
- deptFunctionName: 部門職責名稱(例如:軟體研發部職責)
- deptFunctionBU: 事業體代碼SBU/MBU/HQBU/ITBU/HRBU/ACCBU 之一)
- deptFunctionDept: 部門名稱
- deptManager: 部門主管職稱
- deptMission: 部門使命使用「•」開頭的條列式2-3項
- deptVision: 部門願景使用「•」開頭的條列式1-2項
- deptCoreFunctions: 核心職責使用「•」開頭的條列式4-6項
- deptKPIs: 關鍵績效指標使用「•」開頭的條列式3-4項
請直接返回JSON格式只包含需要生成的欄位不要有任何其他文字
{
${emptyFields.map(f => `"${f}": "..."`).join(',\n ')}
}`;
callClaudeAPI(prompt).then(data => {
let filledCount = 0;
if (fillIfEmpty('deptFunctionCode', data.deptFunctionCode)) filledCount++;
if (fillIfEmpty('deptFunctionName', data.deptFunctionName)) filledCount++;
if (fillIfEmpty('deptFunctionBU', data.deptFunctionBU)) filledCount++;
if (fillIfEmpty('deptFunctionDept', data.deptFunctionDept)) filledCount++;
if (fillIfEmpty('deptManager', data.deptManager)) filledCount++;
if (fillIfEmpty('deptMission', data.deptMission)) filledCount++;
if (fillIfEmpty('deptVision', data.deptVision)) filledCount++;
if (fillIfEmpty('deptCoreFunctions', data.deptCoreFunctions)) filledCount++;
if (fillIfEmpty('deptKPIs', data.deptKPIs)) filledCount++;
showToast(`已自動填入 ${filledCount} 個欄位!`);
}).catch(error => {
showToast('AI 生成失敗: ' + error.message);
}).finally(() => {
setButtonLoading(btn, false);
});
}
function clearDeptFunctionForm() {
document.getElementById('deptFunctionForm').reset();
showToast('表單已清除');
}
function cancelDeptFunction() {
if (confirm('確定要取消編輯嗎?未儲存的資料將會遺失。')) {
clearDeptFunctionForm();
}
}
function saveDeptFunctionAndNew() {
if (!validateDeptFunctionForm()) return;
const formData = getDeptFunctionFormData();
deptFunctionData.push(formData);
showToast('部門職責資料已儲存!');
clearDeptFunctionForm();
// 設定新的編號
const nextCode = 'DF-' + String(deptFunctionData.length + 1).padStart(3, '0');
document.getElementById('deptFunctionCode').value = nextCode;
}
function saveDeptFunctionAndExit() {
if (!validateDeptFunctionForm()) return;
const formData = getDeptFunctionFormData();
deptFunctionData.push(formData);
showToast('部門職責資料已儲存!');
clearDeptFunctionForm();
}
function validateDeptFunctionForm() {
const required = ['deptFunctionCode', 'deptFunctionName', 'deptFunctionBU', 'deptFunctionDept', 'deptFunctionEffectiveDate', 'deptCoreFunctions'];
for (const field of required) {
const el = document.getElementById(field);
if (!el || !el.value.trim()) {
showToast('請填寫必填欄位: ' + field);
el && el.focus();
return false;
}
}
return true;
}
function getDeptFunctionFormData() {
return {
deptFunctionCode: document.getElementById('deptFunctionCode').value,
deptFunctionName: document.getElementById('deptFunctionName').value,
deptFunctionBU: document.getElementById('deptFunctionBU').value,
deptFunctionDept: document.getElementById('deptFunctionDept').value,
deptManager: document.getElementById('deptManager').value,
deptFunctionEffectiveDate: document.getElementById('deptFunctionEffectiveDate').value,
deptHeadcount: document.getElementById('deptHeadcount').value,
deptStatus: document.getElementById('deptStatus').value,
deptMission: document.getElementById('deptMission').value,
deptVision: document.getElementById('deptVision').value,
deptCoreFunctions: document.getElementById('deptCoreFunctions').value,
deptKPIs: document.getElementById('deptKPIs').value,
deptCollaboration: document.getElementById('deptCollaboration').value,
deptFunctionRemark: document.getElementById('deptFunctionRemark').value
};
}
function importDeptFunctionCSV() {
document.getElementById('deptFunctionCsvInput').click();
}
function handleDeptFunctionCSVImport(event) {
const file = event.target.files[0];
if (!file) return;
CSVUtils.importFromCSV(file, (data) => {
if (data && data.length > 0) {
const row = data[0];
Object.keys(row).forEach(key => {
const el = document.getElementById(key);
if (el) el.value = row[key];
});
showToast('已匯入 CSV 資料!');
}
});
event.target.value = '';
}
function exportDeptFunctionCSV() {
const formData = getDeptFunctionFormData();
const headers = Object.keys(formData);
CSVUtils.exportToCSV([formData], 'dept_function.csv', headers);
showToast('部門職責資料已匯出!');
}
// 獲取部門職責清單(供崗位職責選擇使用)
function getDeptFunctionList() {
return deptFunctionData.map(d => ({
code: d.deptFunctionCode,
name: d.deptFunctionName,
dept: d.deptFunctionDept,
bu: d.deptFunctionBU
}));
}
// ==================== 部門職責關聯功能 ====================
// 重新載入部門職責下拉選單
function refreshDeptFunctionList() {
const select = document.getElementById('jd_deptFunction');
if (!select) return;
// 清空現有選項
select.innerHTML = '<option value="">-- 請選擇部門職責 --</option>';
// 從 deptFunctionData 載入選項
if (typeof deptFunctionData !== 'undefined' && deptFunctionData.length > 0) {
deptFunctionData.forEach(df => {
const option = document.createElement('option');
option.value = df.deptFunctionCode;
option.textContent = `${sanitizeHTML(df.deptFunctionCode)} - ${sanitizeHTML(df.deptFunctionName)} (${sanitizeHTML(df.deptFunctionDept)})`;
select.appendChild(option);
});
showToast('已載入 ' + deptFunctionData.length + ' 筆部門職責資料');
} else {
showToast('尚無部門職責資料,請先建立部門職責');
}
}
// 載入選中的部門職責資訊
function loadDeptFunctionInfo() {
const select = document.getElementById('jd_deptFunction');
const infoSection = document.getElementById('deptFunctionInfoSection');
if (!select) return;
const selectedCode = select.value;
if (!selectedCode) {
if (infoSection) infoSection.style.display = 'none';
return;
}
// 從 deptFunctionData 找到對應的資料
if (typeof deptFunctionData !== 'undefined') {
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';
showToast('已載入部門職責: ' + deptFunc.deptFunctionName);
}
}
}
// 在頁面載入時初始化部門職責下拉選單
document.addEventListener('DOMContentLoaded', function() {
// 延遲載入,確保 deptFunctionData 已初始化
setTimeout(refreshDeptFunctionList, 500);
});
// ==================== 管理者頁面功能 ====================
let usersData = [
{ employeeId: 'A001', name: '系統管理員', email: 'admin@company.com', role: 'superadmin', createdAt: '2024-01-01' },
{ employeeId: 'A002', name: '人資主管', email: 'hr_manager@company.com', role: 'admin', createdAt: '2024-01-15' },
{ employeeId: 'A003', name: '一般員工', email: 'employee@company.com', role: 'user', createdAt: '2024-02-01' }
];
let editingUserId = null;
// 顯示新增使用者彈窗
function showAddUserModal() {
editingUserId = null;
document.getElementById('userModalTitle').textContent = '新增使用者';
document.getElementById('userEmployeeId').value = '';
document.getElementById('userName').value = '';
document.getElementById('userEmail').value = '';
document.getElementById('userRole').value = '';
document.getElementById('userEmployeeId').disabled = false;
document.getElementById('userModal').style.display = 'flex';
}
// 編輯使用者
function editUser(employeeId) {
const user = usersData.find(u => u.employeeId === employeeId);
if (!user) return;
editingUserId = employeeId;
document.getElementById('userModalTitle').textContent = '編輯使用者';
document.getElementById('userEmployeeId').value = user.employeeId;
document.getElementById('userEmployeeId').disabled = true;
document.getElementById('userName').value = user.name;
document.getElementById('userEmail').value = user.email;
document.getElementById('userRole').value = user.role;
document.getElementById('userModal').style.display = 'flex';
}
// 關閉使用者彈窗
function closeUserModal() {
document.getElementById('userModal').style.display = 'none';
editingUserId = null;
}
// 儲存使用者
function saveUser(event) {
event.preventDefault();
const employeeId = document.getElementById('userEmployeeId').value;
const name = document.getElementById('userName').value;
const email = document.getElementById('userEmail').value;
const role = document.getElementById('userRole').value;
if (!employeeId || !name || !email || !role) {
showToast('請填寫所有必填欄位');
return;
}
if (editingUserId) {
// 編輯模式
const index = usersData.findIndex(u => u.employeeId === editingUserId);
if (index > -1) {
usersData[index] = { ...usersData[index], name, email, role };
showToast('使用者已更新');
}
} else {
// 新增模式
if (usersData.some(u => u.employeeId === employeeId)) {
showToast('工號已存在');
return;
}
usersData.push({
employeeId,
name,
email,
role,
createdAt: new Date().toISOString().split('T')[0]
});
showToast('使用者已新增');
}
closeUserModal();
renderUserList();
}
// 刪除使用者
function deleteUser(employeeId) {
if (confirm('確定要刪除此使用者嗎?')) {
usersData = usersData.filter(u => u.employeeId !== employeeId);
renderUserList();
showToast('使用者已刪除');
}
}
// 渲染使用者清單
function renderUserList() {
const tbody = document.getElementById('userListBody');
const roleLabels = {
'superadmin': { text: '最高權限管理者', color: '#e74c3c' },
'admin': { text: '管理者', color: '#f39c12' },
'user': { text: '一般使用者', color: '#27ae60' }
};
tbody.innerHTML = usersData.map(user => {
const roleInfo = roleLabels[user.role] || { text: sanitizeHTML(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: ${sanitizeHTML(roleInfo.color)}; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">${sanitizeHTML(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('${sanitizeHTML(user.employeeId)}')">編輯</button>
${!isSuperAdmin ? `<button class="btn btn-cancel" style="padding: 4px 8px; font-size: 12px;" onclick="deleteUser('${sanitizeHTML(user.employeeId)}')">刪除</button>` : ''}
</td>
</tr>
`;
}).join('');
}
// 匯出使用者 CSV
function exportUsersCSV() {
const headers = ['employeeId', 'name', 'email', 'role', 'createdAt'];
CSVUtils.exportToCSV(usersData, 'users.csv', headers);
showToast('使用者清單已匯出!');
}
// ==================== 崗位資料管理功能 ====================
// 匯出完整崗位資料
async function exportCompletePositionData() {
try {
showToast('正在準備匯出資料...');
// 直接使用後端 API 匯出完整崗位資料
window.location.href = API_BASE_URL + '/position-list/export';
setTimeout(() => {
showToast('崗位資料匯出成功!');
}, 1000);
} catch (error) {
console.error('匯出錯誤:', error);
alert('匯出失敗: ' + error.message);
}
}
// 更新崗位統計資料
async function refreshPositionStats() {
try {
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;
document.getElementById('totalPositionsCount').textContent = total;
document.getElementById('describedPositionsCount').textContent = described;
document.getElementById('undescribedPositionsCount').textContent = undescribed;
showToast('統計資料已更新');
} else {
alert('更新統計失敗: ' + (result.error || '未知錯誤'));
}
} catch (error) {
console.error('更新統計錯誤:', error);
alert('更新統計失敗: ' + error.message);
}
}
// 頁面載入時自動更新統計
document.addEventListener('DOMContentLoaded', () => {
// 當切換到管理者模組時自動更新統計
const adminBtn = document.querySelector('.module-btn[data-module="admin"]');
if (adminBtn) {
adminBtn.addEventListener('click', () => {
setTimeout(() => {
refreshPositionStats();
}, 300);
});
}
});
</script>
</body>
</html>