- 新增 CSV 匯入匯出功能(所有頁籤) - 新增崗位清單頁籤(含欄位排序) - 新增管理者頁面(使用者 CRUD) - 新增事業體選項(SBU/MBU/HQBU/ITBU/HRBU/ACCBU) - 新增組織單位欄位(處級/部級/課級) - 崗位描述/備注改為條列式說明 - 新增 README.md 文件 - 新增開發指令記錄檔 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2821 lines
142 KiB
HTML
2821 lines
142 KiB
HTML
<!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="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="請以條列式輸入崗位描述,例如: 1. 負責系統開發與維護 2. 撰寫技術文件 3. 參與專案規劃與執行" rows="6"></textarea>
|
||
</div>
|
||
<div class="form-group full-width">
|
||
<label>崗位備注(條列式說明)</label>
|
||
<textarea id="positionRemark" name="positionRemark" placeholder="請以條列式輸入備注說明,例如: 1. 需具備良好溝通能力 2. 可配合加班 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="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="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-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>
|
||
<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>
|
||
|
||
<!-- 崗位設置目的 -->
|
||
<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="businessUnit" onclick="sortPositionList('businessUnit')" style="padding: 12px; cursor: pointer; text-align: left;">
|
||
事業體 <span class="sort-icon"></span>
|
||
</th>
|
||
<th class="sortable" data-sort="department" onclick="sortPositionList('department')" style="padding: 12px; cursor: pointer; text-align: left;">
|
||
部門 <span class="sort-icon"></span>
|
||
</th>
|
||
<th class="sortable" data-sort="positionCategory" onclick="sortPositionList('positionCategory')" style="padding: 12px; cursor: pointer; text-align: left;">
|
||
崗位類別 <span class="sort-icon"></span>
|
||
</th>
|
||
<th class="sortable" data-sort="headcount" onclick="sortPositionList('headcount')" style="padding: 12px; cursor: pointer; text-align: left;">
|
||
編制人數 <span class="sort-icon"></span>
|
||
</th>
|
||
<th class="sortable" data-sort="effectiveDate" onclick="sortPositionList('effectiveDate')" style="padding: 12px; cursor: pointer; text-align: left;">
|
||
生效日期 <span class="sort-icon"></span>
|
||
</th>
|
||
<th style="padding: 12px; text-align: center;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="positionListBody">
|
||
<tr>
|
||
<td colspan="8" style="padding: 40px; text-align: center; color: var(--text-secondary);">
|
||
點擊「載入清單」按鈕以顯示崗位資料
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ==================== 管理者頁面模組 ==================== -->
|
||
<div class="module-content" id="module-admin">
|
||
<header class="app-header" style="background: linear-gradient(135deg, #c0392b 0%, #e74c3c 100%);">
|
||
<div class="icon">
|
||
<svg viewBox="0 0 24 24" style="fill: white;"><path d="M19.14,12.94c0.04-0.31,0.06-0.63,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>
|
||
</div>
|
||
<div>
|
||
<h1 style="color: white;">管理者頁面</h1>
|
||
<div class="subtitle" style="color: rgba(255,255,255,0.8);">User Administration</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="form-card">
|
||
<div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
|
||
<h2 style="color: var(--primary); margin: 0;">使用者清單</h2>
|
||
<div style="display: flex; gap: 10px;">
|
||
<button type="button" class="btn btn-primary" onclick="showAddUserModal()">
|
||
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
|
||
新增使用者
|
||
</button>
|
||
<button type="button" class="btn btn-secondary" onclick="exportUsersCSV()">
|
||
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
|
||
匯出 CSV
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="overflow-x: auto;">
|
||
<table id="userListTable" style="width: 100%; border-collapse: collapse; background: white;">
|
||
<thead>
|
||
<tr style="background: #c0392b; color: white;">
|
||
<th style="padding: 12px; text-align: left;">工號</th>
|
||
<th style="padding: 12px; text-align: left;">使用者姓名</th>
|
||
<th style="padding: 12px; text-align: left;">Email 信箱</th>
|
||
<th style="padding: 12px; text-align: left;">權限等級</th>
|
||
<th style="padding: 12px; text-align: left;">建立日期</th>
|
||
<th style="padding: 12px; text-align: center;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="userListBody">
|
||
<tr>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">A001</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">系統管理員</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">admin@company.com</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">
|
||
<span style="background: #e74c3c; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">最高權限管理者</span>
|
||
</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">2024-01-01</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">
|
||
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="editUser('A001')">編輯</button>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">A002</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">人資主管</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">hr_manager@company.com</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">
|
||
<span style="background: #f39c12; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">管理者</span>
|
||
</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">2024-01-15</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">
|
||
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="editUser('A002')">編輯</button>
|
||
<button class="btn btn-cancel" style="padding: 4px 8px; font-size: 12px;" onclick="deleteUser('A002')">刪除</button>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">A003</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">一般員工</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">employee@company.com</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">
|
||
<span style="background: #27ae60; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">一般使用者</span>
|
||
</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">2024-02-01</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">
|
||
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="editUser('A003')">編輯</button>
|
||
<button class="btn btn-cancel" style="padding: 4px 8px; font-size: 12px;" onclick="deleteUser('A003')">刪除</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增/編輯使用者彈窗 -->
|
||
<div id="userModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center;">
|
||
<div style="background: white; border-radius: 8px; padding: 30px; max-width: 500px; width: 90%;">
|
||
<h3 id="userModalTitle" style="margin: 0 0 20px 0; color: var(--primary);">新增使用者</h3>
|
||
<form id="userForm">
|
||
<div style="margin-bottom: 15px;">
|
||
<label style="display: block; margin-bottom: 5px; font-weight: 500;">工號 <span style="color: red;">*</span></label>
|
||
<input type="text" id="userEmployeeId" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||
</div>
|
||
<div style="margin-bottom: 15px;">
|
||
<label style="display: block; margin-bottom: 5px; font-weight: 500;">使用者姓名 <span style="color: red;">*</span></label>
|
||
<input type="text" id="userName" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||
</div>
|
||
<div style="margin-bottom: 15px;">
|
||
<label style="display: block; margin-bottom: 5px; font-weight: 500;">Email 信箱 <span style="color: red;">*</span></label>
|
||
<input type="email" id="userEmail" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||
</div>
|
||
<div style="margin-bottom: 20px;">
|
||
<label style="display: block; margin-bottom: 5px; font-weight: 500;">權限等級 <span style="color: red;">*</span></label>
|
||
<select id="userRole" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||
<option value="">請選擇</option>
|
||
<option value="user">一般使用者</option>
|
||
<option value="admin">管理者</option>
|
||
<option value="superadmin">最高權限管理者</option>
|
||
</select>
|
||
</div>
|
||
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
||
<button type="button" class="btn btn-cancel" onclick="closeUserModal()">取消</button>
|
||
<button type="submit" class="btn btn-primary" onclick="saveUser(event)">儲存</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<script>
|
||
// ==================== 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 ====================
|
||
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');
|
||
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();
|
||
}
|
||
}
|
||
|
||
function saveJobDescAndExit() {
|
||
console.log('Save JobDesc:', getJobDescFormData());
|
||
showToast('崗位描述已保存!');
|
||
}
|
||
|
||
function saveJobDescAndNew() {
|
||
console.log('Save JobDesc:', getJobDescFormData());
|
||
showToast('崗位描述已保存,請繼續新增!');
|
||
document.getElementById('jobDescForm').reset();
|
||
document.getElementById('jd_mainResponsibilities').value = '1、\n2、\n3、\n4、';
|
||
updatePreview();
|
||
}
|
||
|
||
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;">${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;
|
||
">${message}</div>
|
||
|
||
${suggestions && suggestions.length > 0 ? `
|
||
<div style="
|
||
background: #fff3cd;
|
||
border: 1px solid #ffc107;
|
||
border-radius: 6px;
|
||
padding: 15px;
|
||
margin-bottom: 20px;
|
||
">
|
||
<strong style="color: #856404; display: block; margin-bottom: 10px;">💡 請確保:</strong>
|
||
<ul style="margin: 0; padding-left: 20px; color: #856404;">
|
||
${suggestions.map(s => `<li style="margin: 5px 0;">${s}</li>`).join('')}
|
||
</ul>
|
||
</div>
|
||
` : ''}
|
||
|
||
${details ? `
|
||
<details style="
|
||
background: #f8f9fa;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 6px;
|
||
padding: 15px;
|
||
">
|
||
<summary style="
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
user-select: none;
|
||
margin-bottom: 10px;
|
||
">🔍 詳細錯誤訊息(點擊展開)</summary>
|
||
<pre id="errorDetailsText" style="
|
||
background: white;
|
||
padding: 15px;
|
||
border-radius: 4px;
|
||
overflow-x: auto;
|
||
font-size: 0.85rem;
|
||
color: #666;
|
||
margin: 10px 0 0 0;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
">${details}</pre>
|
||
<button onclick="copyErrorDetails()" style="
|
||
background: #007bff;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
margin-top: 10px;
|
||
font-size: 0.9rem;
|
||
">📋 複製錯誤訊息</button>
|
||
</details>
|
||
` : ''}
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div style="
|
||
padding: 15px 25px;
|
||
border-top: 1px solid #f0f0f0;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
">
|
||
<button onclick="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 exportPositionsCSV() {
|
||
// 收集所有崗位資料(這裡簡化為當前表單資料)
|
||
const data = [{
|
||
positionCode: getFieldValue('positionCode'),
|
||
positionName: getFieldValue('positionName'),
|
||
positionCategory: getFieldValue('positionCategory'),
|
||
positionNature: getFieldValue('positionNature'),
|
||
headcount: getFieldValue('headcount'),
|
||
positionLevel: getFieldValue('positionLevel'),
|
||
effectiveDate: getFieldValue('effectiveDate'),
|
||
positionDesc: getFieldValue('positionDesc'),
|
||
positionRemark: getFieldValue('positionRemark'),
|
||
minEducation: getFieldValue('minEducation'),
|
||
salaryRange: getFieldValue('salaryRange'),
|
||
workExperience: getFieldValue('workExperience'),
|
||
minAge: getFieldValue('minAge'),
|
||
maxAge: getFieldValue('maxAge')
|
||
}];
|
||
|
||
const headers = ['positionCode', 'positionName', 'positionCategory', 'positionNature',
|
||
'headcount', 'positionLevel', 'effectiveDate', 'positionDesc', 'positionRemark',
|
||
'minEducation', 'salaryRange', 'workExperience', 'minAge', 'maxAge'];
|
||
|
||
CSVUtils.exportToCSV(data, 'positions.csv', headers);
|
||
showToast('崗位資料已匯出!');
|
||
}
|
||
|
||
// 崗位資料 CSV 匯入觸發
|
||
function importPositionsCSV() {
|
||
document.getElementById('positionCSVInput').click();
|
||
}
|
||
|
||
// 處理崗位 CSV 匯入
|
||
function handlePositionCSVImport(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
CSVUtils.importFromCSV(file, (data) => {
|
||
if (data && data.length > 0) {
|
||
const firstRow = data[0];
|
||
// 填充表單
|
||
Object.keys(firstRow).forEach(key => {
|
||
const element = document.getElementById(key);
|
||
if (element) {
|
||
element.value = firstRow[key];
|
||
}
|
||
});
|
||
showToast(`已匯入 ${data.length} 筆崗位資料(顯示第一筆)`);
|
||
}
|
||
});
|
||
// 重置 input
|
||
event.target.value = '';
|
||
}
|
||
|
||
// 職務資料 CSV 匯出
|
||
function exportJobsCSV() {
|
||
const data = [{
|
||
jobCategoryCode: getFieldValue('jobCategoryCode'),
|
||
jobCategoryName: getFieldValue('jobCategoryName'),
|
||
jobCode: getFieldValue('jobCode'),
|
||
jobName: getFieldValue('jobName'),
|
||
jobNameEn: getFieldValue('jobNameEn'),
|
||
jobEffectiveDate: getFieldValue('jobEffectiveDate'),
|
||
jobHeadcount: getFieldValue('jobHeadcount'),
|
||
jobSortOrder: getFieldValue('jobSortOrder'),
|
||
jobRemark: getFieldValue('jobRemark'),
|
||
jobLevel: getFieldValue('jobLevel'),
|
||
hasAttendanceBonus: document.getElementById('hasAttendanceBonus')?.checked,
|
||
hasHousingAllowance: document.getElementById('hasHousingAllowance')?.checked
|
||
}];
|
||
|
||
const headers = ['jobCategoryCode', 'jobCategoryName', 'jobCode', 'jobName', 'jobNameEn',
|
||
'jobEffectiveDate', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel',
|
||
'hasAttendanceBonus', 'hasHousingAllowance'];
|
||
|
||
CSVUtils.exportToCSV(data, 'jobs.csv', headers);
|
||
showToast('職務資料已匯出!');
|
||
}
|
||
|
||
// 職務資料 CSV 匯入觸發
|
||
function importJobsCSV() {
|
||
document.getElementById('jobCSVInput').click();
|
||
}
|
||
|
||
// 處理職務 CSV 匯入
|
||
function handleJobCSVImport(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
CSVUtils.importFromCSV(file, (data) => {
|
||
if (data && data.length > 0) {
|
||
const firstRow = data[0];
|
||
Object.keys(firstRow).forEach(key => {
|
||
const element = document.getElementById(key);
|
||
if (element) {
|
||
if (element.type === 'checkbox') {
|
||
element.checked = firstRow[key] === 'true';
|
||
} else {
|
||
element.value = firstRow[key];
|
||
}
|
||
}
|
||
});
|
||
showToast(`已匯入 ${data.length} 筆職務資料(顯示第一筆)`);
|
||
}
|
||
});
|
||
event.target.value = '';
|
||
}
|
||
|
||
// 崗位描述 CSV 匯出
|
||
function exportDescriptionsCSV() {
|
||
const data = [{
|
||
descPositionCode: getFieldValue('descPositionCode'),
|
||
descPositionName: getFieldValue('descPositionName'),
|
||
descEffectiveDate: getFieldValue('descEffectiveDate'),
|
||
jobDuties: getFieldValue('jobDuties'),
|
||
requiredSkills: getFieldValue('requiredSkills'),
|
||
workEnvironment: getFieldValue('workEnvironment'),
|
||
careerPath: getFieldValue('careerPath'),
|
||
descRemark: getFieldValue('descRemark')
|
||
}];
|
||
|
||
const headers = ['descPositionCode', 'descPositionName', 'descEffectiveDate', 'jobDuties',
|
||
'requiredSkills', 'workEnvironment', 'careerPath', 'descRemark'];
|
||
|
||
CSVUtils.exportToCSV(data, 'job_descriptions.csv', headers);
|
||
showToast('崗位描述已匯出!');
|
||
}
|
||
|
||
// 崗位描述 CSV 匯入觸發
|
||
function importDescriptionsCSV() {
|
||
document.getElementById('descCSVInput').click();
|
||
}
|
||
|
||
// 處理崗位描述 CSV 匯入
|
||
function handleDescCSVImport(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
CSVUtils.importFromCSV(file, (data) => {
|
||
if (data && data.length > 0) {
|
||
const firstRow = data[0];
|
||
Object.keys(firstRow).forEach(key => {
|
||
const element = document.getElementById(key);
|
||
if (element) {
|
||
element.value = firstRow[key];
|
||
}
|
||
});
|
||
showToast(`已匯入 ${data.length} 筆崗位描述資料(顯示第一筆)`);
|
||
}
|
||
});
|
||
event.target.value = '';
|
||
}
|
||
|
||
|
||
// ==================== 崗位清單功能 ====================
|
||
let positionListData = [];
|
||
let currentSortColumn = '';
|
||
let currentSortDirection = 'asc';
|
||
|
||
// 載入崗位清單(示範資料)
|
||
function loadPositionList() {
|
||
// 示範資料
|
||
positionListData = [
|
||
{ positionCode: 'POS001', positionName: '軟體工程師', businessUnit: 'ITBU', department: '研發部', positionCategory: '技術職', headcount: 5, effectiveDate: '2024-01-01' },
|
||
{ positionCode: 'POS002', positionName: '專案經理', businessUnit: 'ITBU', department: '專案管理部', positionCategory: '管理職', headcount: 2, effectiveDate: '2024-01-01' },
|
||
{ positionCode: 'POS003', positionName: '人資專員', businessUnit: 'HRBU', department: '人力資源部', positionCategory: '行政職', headcount: 3, effectiveDate: '2024-02-01' },
|
||
{ positionCode: 'POS004', positionName: '財務分析師', businessUnit: 'ACCBU', department: '財務部', positionCategory: '專業職', headcount: 2, effectiveDate: '2024-01-15' },
|
||
{ positionCode: 'POS005', positionName: '業務代表', businessUnit: 'SBU', department: '業務部', positionCategory: '業務職', headcount: 10, effectiveDate: '2024-03-01' },
|
||
{ positionCode: 'POS006', positionName: '生產線主管', businessUnit: 'MBU', department: '生產部', positionCategory: '管理職', headcount: 4, effectiveDate: '2024-01-01' },
|
||
];
|
||
renderPositionList();
|
||
showToast('已載入 ' + positionListData.length + ' 筆崗位資料');
|
||
}
|
||
|
||
// 渲染崗位清單
|
||
function renderPositionList() {
|
||
const tbody = document.getElementById('positionListBody');
|
||
if (positionListData.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="8" style="padding: 40px; text-align: center; color: var(--text-secondary);">沒有資料</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = positionListData.map(item => `
|
||
<tr style="border-bottom: 1px solid #eee;">
|
||
<td style="padding: 12px;">${item.positionCode}</td>
|
||
<td style="padding: 12px;">${item.positionName}</td>
|
||
<td style="padding: 12px;">${item.businessUnit}</td>
|
||
<td style="padding: 12px;">${item.department}</td>
|
||
<td style="padding: 12px;">${item.positionCategory}</td>
|
||
<td style="padding: 12px;">${item.headcount}</td>
|
||
<td style="padding: 12px;">${item.effectiveDate}</td>
|
||
<td style="padding: 12px; text-align: center;">
|
||
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="viewPosition('${item.positionCode}')">檢視</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
// 排序崗位清單
|
||
function sortPositionList(column) {
|
||
if (currentSortColumn === column) {
|
||
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
currentSortColumn = column;
|
||
currentSortDirection = 'asc';
|
||
}
|
||
|
||
positionListData.sort((a, b) => {
|
||
let valA = a[column];
|
||
let valB = b[column];
|
||
|
||
if (typeof valA === 'string') {
|
||
valA = valA.toLowerCase();
|
||
valB = valB.toLowerCase();
|
||
}
|
||
|
||
if (valA < valB) return currentSortDirection === 'asc' ? -1 : 1;
|
||
if (valA > valB) return currentSortDirection === 'asc' ? 1 : -1;
|
||
return 0;
|
||
});
|
||
|
||
// 更新排序圖示
|
||
document.querySelectorAll('.sort-icon').forEach(icon => icon.textContent = '');
|
||
const currentHeader = document.querySelector(`th[data-sort="${column}"] .sort-icon`);
|
||
if (currentHeader) {
|
||
currentHeader.textContent = currentSortDirection === 'asc' ? ' ^' : ' v';
|
||
}
|
||
|
||
renderPositionList();
|
||
}
|
||
|
||
// 檢視崗位
|
||
function viewPosition(code) {
|
||
const position = positionListData.find(p => p.positionCode === code);
|
||
if (position) {
|
||
showToast('檢視崗位: ' + position.positionName);
|
||
}
|
||
}
|
||
|
||
// 匯出崗位清單 CSV
|
||
function exportPositionListCSV() {
|
||
if (positionListData.length === 0) {
|
||
showToast('請先載入清單資料');
|
||
return;
|
||
}
|
||
const headers = ['positionCode', 'positionName', 'businessUnit', 'department', 'positionCategory', 'headcount', 'effectiveDate'];
|
||
CSVUtils.exportToCSV(positionListData, 'position_list.csv', headers);
|
||
showToast('崗位清單已匯出!');
|
||
}
|
||
|
||
// ==================== 管理者頁面功能 ====================
|
||
let usersData = [
|
||
{ employeeId: 'A001', name: '系統管理員', email: 'admin@company.com', role: 'superadmin', createdAt: '2024-01-01' },
|
||
{ employeeId: 'A002', name: '人資主管', email: 'hr_manager@company.com', role: 'admin', createdAt: '2024-01-15' },
|
||
{ employeeId: 'A003', name: '一般員工', email: 'employee@company.com', role: 'user', createdAt: '2024-02-01' }
|
||
];
|
||
let editingUserId = null;
|
||
|
||
// 顯示新增使用者彈窗
|
||
function showAddUserModal() {
|
||
editingUserId = null;
|
||
document.getElementById('userModalTitle').textContent = '新增使用者';
|
||
document.getElementById('userEmployeeId').value = '';
|
||
document.getElementById('userName').value = '';
|
||
document.getElementById('userEmail').value = '';
|
||
document.getElementById('userRole').value = '';
|
||
document.getElementById('userEmployeeId').disabled = false;
|
||
document.getElementById('userModal').style.display = 'flex';
|
||
}
|
||
|
||
// 編輯使用者
|
||
function editUser(employeeId) {
|
||
const user = usersData.find(u => u.employeeId === employeeId);
|
||
if (!user) return;
|
||
|
||
editingUserId = employeeId;
|
||
document.getElementById('userModalTitle').textContent = '編輯使用者';
|
||
document.getElementById('userEmployeeId').value = user.employeeId;
|
||
document.getElementById('userEmployeeId').disabled = true;
|
||
document.getElementById('userName').value = user.name;
|
||
document.getElementById('userEmail').value = user.email;
|
||
document.getElementById('userRole').value = user.role;
|
||
document.getElementById('userModal').style.display = 'flex';
|
||
}
|
||
|
||
// 關閉使用者彈窗
|
||
function closeUserModal() {
|
||
document.getElementById('userModal').style.display = 'none';
|
||
editingUserId = null;
|
||
}
|
||
|
||
// 儲存使用者
|
||
function saveUser(event) {
|
||
event.preventDefault();
|
||
const employeeId = document.getElementById('userEmployeeId').value;
|
||
const name = document.getElementById('userName').value;
|
||
const email = document.getElementById('userEmail').value;
|
||
const role = document.getElementById('userRole').value;
|
||
|
||
if (!employeeId || !name || !email || !role) {
|
||
showToast('請填寫所有必填欄位');
|
||
return;
|
||
}
|
||
|
||
if (editingUserId) {
|
||
// 編輯模式
|
||
const index = usersData.findIndex(u => u.employeeId === editingUserId);
|
||
if (index > -1) {
|
||
usersData[index] = { ...usersData[index], name, email, role };
|
||
showToast('使用者已更新');
|
||
}
|
||
} else {
|
||
// 新增模式
|
||
if (usersData.some(u => u.employeeId === employeeId)) {
|
||
showToast('工號已存在');
|
||
return;
|
||
}
|
||
usersData.push({
|
||
employeeId,
|
||
name,
|
||
email,
|
||
role,
|
||
createdAt: new Date().toISOString().split('T')[0]
|
||
});
|
||
showToast('使用者已新增');
|
||
}
|
||
|
||
closeUserModal();
|
||
renderUserList();
|
||
}
|
||
|
||
// 刪除使用者
|
||
function deleteUser(employeeId) {
|
||
if (confirm('確定要刪除此使用者嗎?')) {
|
||
usersData = usersData.filter(u => u.employeeId !== employeeId);
|
||
renderUserList();
|
||
showToast('使用者已刪除');
|
||
}
|
||
}
|
||
|
||
// 渲染使用者清單
|
||
function renderUserList() {
|
||
const tbody = document.getElementById('userListBody');
|
||
const roleLabels = {
|
||
'superadmin': { text: '最高權限管理者', color: '#e74c3c' },
|
||
'admin': { text: '管理者', color: '#f39c12' },
|
||
'user': { text: '一般使用者', color: '#27ae60' }
|
||
};
|
||
|
||
tbody.innerHTML = usersData.map(user => {
|
||
const roleInfo = roleLabels[user.role] || { text: user.role, color: '#999' };
|
||
const isSuperAdmin = user.role === 'superadmin';
|
||
return `
|
||
<tr>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">${user.employeeId}</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">${user.name}</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">${user.email}</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">
|
||
<span style="background: ${roleInfo.color}; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">${roleInfo.text}</span>
|
||
</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee;">${user.createdAt}</td>
|
||
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">
|
||
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="editUser('${user.employeeId}')">編輯</button>
|
||
${!isSuperAdmin ? `<button class="btn btn-cancel" style="padding: 4px 8px; font-size: 12px;" onclick="deleteUser('${user.employeeId}')">刪除</button>` : ''}
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// 匯出使用者 CSV
|
||
function exportUsersCSV() {
|
||
const headers = ['employeeId', 'name', 'email', 'role', 'createdAt'];
|
||
CSVUtils.exportToCSV(usersData, 'users.csv', headers);
|
||
showToast('使用者清單已匯出!');
|
||
}
|
||
|
||
|
||
</script>
|
||
</body>
|
||
</html>
|