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:
644
index.html
644
index.html
@@ -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="請以條列式輸入崗位描述,例如: 1. 負責系統開發與維護 2. 撰寫技術文件 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="請以條列式輸入備注說明,例如: 1. 需具備良好溝通能力 2. 可配合加班 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>
|
||||
|
||||
Reference in New Issue
Block a user