feat: 新增多項功能 v2.1

- 新增 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>
This commit is contained in:
2025-12-04 10:06:50 +08:00
parent 293d64bc65
commit d17af39bf4
8 changed files with 2302 additions and 4 deletions

View File

@@ -606,6 +606,7 @@
.module-btn { min-width: 100%; }
}
</style>
<script src="csv_utils.js"></script>
</head>
<body>
<div class="app-container">
@@ -623,6 +624,14 @@
<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>
<!-- ==================== 崗位基礎資料模組 ==================== -->
@@ -650,6 +659,38 @@
<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">
@@ -712,12 +753,12 @@
</div>
<div class="form-group"></div>
<div class="form-group full-width">
<label>崗位描述</label>
<textarea id="positionDesc" name="positionDesc" placeholder="請輸入崗位描述..." rows="4"></textarea>
<label>崗位描述(條列式說明)</label>
<textarea id="positionDesc" name="positionDesc" placeholder="請以條列式輸入崗位描述,例如:&#10;1. 負責系統開發與維護&#10;2. 撰寫技術文件&#10;3. 參與專案規劃與執行" rows="6"></textarea>
</div>
<div class="form-group full-width">
<label>崗位備注</label>
<textarea id="positionRemark" name="positionRemark" placeholder="請輸入備注說明..." rows="5"></textarea>
<label>崗位備注(條列式說明)</label>
<textarea id="positionRemark" name="positionRemark" placeholder="請以條列式輸入備注說明,例如:&#10;1. 需具備良好溝通能力&#10;2. 可配合加班&#10;3. 其他注意事項" rows="6"></textarea>
</div>
</div>
</div>
@@ -859,6 +900,18 @@
<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>
@@ -986,6 +1039,18 @@
<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>
@@ -1259,6 +1324,195 @@
<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') {
@@ -2179,6 +2433,388 @@ ${contextInfo}
});
}
// ==================== 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>