變更內容: - 所有資料表加上 HR_position_ 前綴 - 整理完整欄位顯示名稱與 ID 對照表 - 模組化 JS 檔案 (admin.js, ai.js, csv.js 等) - 專案結構優化 (docs/, scripts/, tests/ 等) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
572 lines
21 KiB
JavaScript
572 lines
21 KiB
JavaScript
/**
|
|
* 表單邏輯模組
|
|
* 處理各表單的資料操作、驗證和提交
|
|
*/
|
|
|
|
const API_BASE_URL = '/api';
|
|
|
|
// ==================== 常數映射 ====================
|
|
|
|
export const categoryMap = { '01': '技術職', '02': '管理職', '03': '業務職', '04': '行政職' };
|
|
export const natureMap = { 'FT': '全職', 'PT': '兼職', 'CT': '約聘', 'IN': '實習' };
|
|
export const jobCategoryMap = { 'MGR': '管理職', 'TECH': '技術職', 'SALE': '業務職', 'ADMIN': '行政職', 'RD': '研發職', 'PROD': '生產職' };
|
|
|
|
// ==================== 崗位清單全域變數 ====================
|
|
export let positionListData = [];
|
|
export let currentSortColumn = '';
|
|
export let currentSortDirection = 'asc';
|
|
|
|
// ==================== 崗位基礎資料表單 ====================
|
|
|
|
export function updateCategoryName() {
|
|
const category = document.getElementById('positionCategory').value;
|
|
document.getElementById('positionCategoryName').value = categoryMap[category] || '';
|
|
updatePreview();
|
|
}
|
|
|
|
export function updateNatureName() {
|
|
const nature = document.getElementById('positionNature').value;
|
|
document.getElementById('positionNatureName').value = natureMap[nature] || '';
|
|
updatePreview();
|
|
}
|
|
|
|
export function changePositionCode() {
|
|
const currentCode = document.getElementById('positionCode').value;
|
|
const newCode = prompt('請輸入新的崗位編號:', currentCode);
|
|
if (newCode && newCode !== currentCode) {
|
|
document.getElementById('positionCode').value = newCode;
|
|
showToast('崗位編號已更改!');
|
|
updatePreview();
|
|
}
|
|
}
|
|
|
|
export function getPositionFormData() {
|
|
const form = document.getElementById('positionForm');
|
|
const formData = new FormData(form);
|
|
const data = { basicInfo: {}, recruitInfo: {} };
|
|
const basicFields = ['positionCode', 'positionName', 'positionCategory', 'positionCategoryName', 'positionNature', 'positionNatureName', 'headcount', 'positionLevel', 'effectiveDate', 'positionDesc', 'positionRemark'];
|
|
const recruitFields = ['minEducation', 'requiredGender', 'salaryRange', 'workExperience', 'minAge', 'maxAge', 'jobType', 'recruitPosition', 'jobTitle', 'jobDesc', 'positionReq', 'titleReq', 'majorReq', 'skillReq', 'langReq', 'otherReq', 'superiorPosition', 'recruitRemark'];
|
|
basicFields.forEach(field => { const value = formData.get(field); if (value) data.basicInfo[field] = value; });
|
|
recruitFields.forEach(field => { const value = formData.get(field); if (value) data.recruitInfo[field] = value; });
|
|
return data;
|
|
}
|
|
|
|
export function savePositionAndExit() {
|
|
const form = document.getElementById('positionForm');
|
|
if (!form.checkValidity()) { form.reportValidity(); return; }
|
|
console.log('Save Position:', getPositionFormData());
|
|
showToast('崗位資料已保存!');
|
|
}
|
|
|
|
export function savePositionAndNew() {
|
|
const form = document.getElementById('positionForm');
|
|
if (!form.checkValidity()) { form.reportValidity(); return; }
|
|
console.log('Save Position:', getPositionFormData());
|
|
showToast('崗位資料已保存,請繼續新增!');
|
|
form.reset();
|
|
document.getElementById('effectiveDate').value = new Date().toISOString().split('T')[0];
|
|
updatePreview();
|
|
}
|
|
|
|
export async function saveToPositionList() {
|
|
const form = document.getElementById('positionForm');
|
|
if (!form.checkValidity()) {
|
|
form.reportValidity();
|
|
return;
|
|
}
|
|
|
|
const formData = getPositionFormData();
|
|
|
|
if (!formData.basicInfo.positionCode) {
|
|
alert('請輸入崗位編號');
|
|
return;
|
|
}
|
|
if (!formData.basicInfo.positionName) {
|
|
alert('請輸入崗位名稱');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(API_BASE_URL + '/positions', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(formData)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast(result.message || '崗位已成功儲存至崗位清單!');
|
|
setTimeout(() => {
|
|
switchModule('positionlist');
|
|
}, 1500);
|
|
} else {
|
|
alert('儲存失敗: ' + (result.error || '未知錯誤'));
|
|
}
|
|
} catch (error) {
|
|
console.error('儲存錯誤:', error);
|
|
alert('儲存失敗: ' + error.message);
|
|
}
|
|
}
|
|
|
|
export function cancelPositionForm() {
|
|
if (confirm('確定要取消嗎?未保存的資料將會遺失。')) {
|
|
document.getElementById('positionForm').reset();
|
|
updatePreview();
|
|
}
|
|
}
|
|
|
|
// ==================== 職務基礎資料表單 ====================
|
|
|
|
export function updateJobCategoryName() {
|
|
const category = document.getElementById('jobCategoryCode').value;
|
|
document.getElementById('jobCategoryName').value = jobCategoryMap[category] || '';
|
|
updatePreview();
|
|
}
|
|
|
|
export function changeJobCode() {
|
|
const currentCode = document.getElementById('jobCode').value;
|
|
const newCode = prompt('請輸入新的職務編號:', currentCode);
|
|
if (newCode && newCode !== currentCode) {
|
|
document.getElementById('jobCode').value = newCode;
|
|
showToast('職務編號已更改!');
|
|
updatePreview();
|
|
}
|
|
}
|
|
|
|
export function getJobFormData() {
|
|
const form = document.getElementById('jobForm');
|
|
const formData = new FormData(form);
|
|
const data = {};
|
|
const fields = ['jobCategoryCode', 'jobCategoryName', 'jobCode', 'jobName', 'jobNameEn', 'jobEffectiveDate', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel'];
|
|
fields.forEach(field => { const value = formData.get(field); if (value) data[field] = value; });
|
|
data.hasAttendanceBonus = document.getElementById('job_hasAttBonus')?.checked || false;
|
|
data.hasHousingAllowance = document.getElementById('job_hasHouseAllow')?.checked || false;
|
|
return data;
|
|
}
|
|
|
|
export async function saveJobToPositionList() {
|
|
const form = document.getElementById('jobForm');
|
|
if (!form.checkValidity()) {
|
|
form.reportValidity();
|
|
return;
|
|
}
|
|
|
|
const formData = getJobFormData();
|
|
|
|
if (!formData.jobCode) {
|
|
alert('請輸入職務代碼');
|
|
return;
|
|
}
|
|
if (!formData.jobName) {
|
|
alert('請輸入職務名稱');
|
|
return;
|
|
}
|
|
|
|
const positionData = {
|
|
basicInfo: {
|
|
positionCode: formData.jobCode,
|
|
positionName: formData.jobName,
|
|
positionCategory: formData.jobCategoryCode || '',
|
|
effectiveDate: formData.jobEffectiveDate || new Date().toISOString().split('T')[0],
|
|
headcount: formData.jobHeadcount || 1,
|
|
positionLevel: formData.jobLevel || '',
|
|
positionRemark: formData.jobRemark || ''
|
|
},
|
|
recruitInfo: {}
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(API_BASE_URL + '/positions', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(positionData)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast(result.message || '職務已成功儲存至崗位清單!');
|
|
setTimeout(() => {
|
|
switchModule('positionlist');
|
|
}, 1500);
|
|
} else {
|
|
alert('儲存失敗: ' + (result.error || '未知錯誤'));
|
|
}
|
|
} catch (error) {
|
|
console.error('儲存錯誤:', error);
|
|
alert('儲存失敗: ' + error.message);
|
|
}
|
|
}
|
|
|
|
export function saveJobAndExit() {
|
|
const form = document.getElementById('jobForm');
|
|
if (!form.checkValidity()) { form.reportValidity(); return; }
|
|
console.log('Save Job:', getJobFormData());
|
|
showToast('職務資料已保存!');
|
|
}
|
|
|
|
export function saveJobAndNew() {
|
|
const form = document.getElementById('jobForm');
|
|
if (!form.checkValidity()) { form.reportValidity(); return; }
|
|
console.log('Save Job:', getJobFormData());
|
|
showToast('職務資料已保存,請繼續新增!');
|
|
form.reset();
|
|
document.getElementById('attendanceLabel').textContent = '否';
|
|
document.getElementById('housingLabel').textContent = '否';
|
|
updatePreview();
|
|
}
|
|
|
|
export function cancelJobForm() {
|
|
if (confirm('確定要取消嗎?未保存的資料將會遺失。')) {
|
|
document.getElementById('jobForm').reset();
|
|
document.getElementById('attendanceLabel').textContent = '否';
|
|
document.getElementById('housingLabel').textContent = '否';
|
|
updatePreview();
|
|
}
|
|
}
|
|
|
|
// ==================== 崗位描述表單 ====================
|
|
|
|
export function getJobDescFormData() {
|
|
const form = document.getElementById('jobDescForm');
|
|
const formData = new FormData(form);
|
|
const data = { basicInfo: {}, positionInfo: {}, responsibilities: {}, requirements: {} };
|
|
|
|
['empNo', 'empName', 'positionCode', 'versionDate'].forEach(field => {
|
|
const el = document.getElementById('jd_' + field);
|
|
if (el && el.value) data.basicInfo[field] = el.value;
|
|
});
|
|
|
|
['positionName', 'department', 'positionEffectiveDate', 'directSupervisor', 'positionGradeJob', 'reportTo', 'directReports', 'workLocation', 'empAttribute'].forEach(field => {
|
|
const el = document.getElementById('jd_' + field);
|
|
if (el && el.value) data.positionInfo[field] = el.value;
|
|
});
|
|
|
|
const purpose = document.getElementById('jd_positionPurpose');
|
|
if (purpose && purpose.value) data.responsibilities.positionPurpose = purpose.value;
|
|
const mainResp = document.getElementById('jd_mainResponsibilities');
|
|
if (mainResp && mainResp.value) data.responsibilities.mainResponsibilities = mainResp.value;
|
|
|
|
['education', 'basicSkills', 'professionalKnowledge', 'workExperienceReq', 'otherRequirements'].forEach(field => {
|
|
const el = document.getElementById('jd_' + field);
|
|
if (el && el.value) data.requirements[field] = el.value;
|
|
});
|
|
|
|
return data;
|
|
}
|
|
|
|
export async function saveJobDescAndExit() {
|
|
const formData = getJobDescFormData();
|
|
console.log('Save JobDesc:', formData);
|
|
|
|
if (!formData.basicInfo.positionCode) {
|
|
alert('請輸入崗位代碼');
|
|
return;
|
|
}
|
|
|
|
const requestData = {
|
|
positionCode: formData.basicInfo.positionCode,
|
|
positionName: formData.positionInfo.positionName || '',
|
|
effectiveDate: formData.positionInfo.positionEffectiveDate || new Date().toISOString().split('T')[0],
|
|
jobDuties: formData.responsibilities.mainResponsibilities || '',
|
|
requiredSkills: formData.requirements.basicSkills || '',
|
|
workEnvironment: formData.positionInfo.workLocation || ''
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(API_BASE_URL + '/position-descriptions', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(requestData)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast(result.message || '崗位描述已保存!');
|
|
setTimeout(() => {
|
|
switchModule('positionlist');
|
|
}, 1000);
|
|
} else {
|
|
alert('保存失敗: ' + (result.error || '未知錯誤'));
|
|
}
|
|
} catch (error) {
|
|
console.error('保存錯誤:', error);
|
|
alert('保存失敗: ' + error.message);
|
|
}
|
|
}
|
|
|
|
export async function saveJobDescAndNew() {
|
|
const formData = getJobDescFormData();
|
|
console.log('Save JobDesc:', formData);
|
|
|
|
if (!formData.basicInfo.positionCode) {
|
|
alert('請輸入崗位代碼');
|
|
return;
|
|
}
|
|
|
|
const requestData = {
|
|
positionCode: formData.basicInfo.positionCode,
|
|
positionName: formData.positionInfo.positionName || '',
|
|
effectiveDate: formData.positionInfo.positionEffectiveDate || new Date().toISOString().split('T')[0],
|
|
jobDuties: formData.responsibilities.mainResponsibilities || '',
|
|
requiredSkills: formData.requirements.basicSkills || '',
|
|
workEnvironment: formData.positionInfo.workLocation || ''
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(API_BASE_URL + '/position-descriptions', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(requestData)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast(result.message || '崗位描述已保存,請繼續新增!');
|
|
document.getElementById('jobDescForm').reset();
|
|
document.getElementById('jd_mainResponsibilities').value = '1、\n2、\n3、\n4、';
|
|
updatePreview();
|
|
} else {
|
|
alert('保存失敗: ' + (result.error || '未知錯誤'));
|
|
}
|
|
} catch (error) {
|
|
console.error('保存錯誤:', error);
|
|
alert('保存失敗: ' + error.message);
|
|
}
|
|
}
|
|
|
|
export async function saveJobDescToPositionList() {
|
|
const formData = getJobDescFormData();
|
|
|
|
if (!formData.basicInfo.positionCode) {
|
|
alert('請輸入崗位代碼');
|
|
return;
|
|
}
|
|
|
|
const positionData = {
|
|
basicInfo: {
|
|
positionCode: formData.basicInfo.positionCode,
|
|
positionName: formData.positionInfo.positionName || '',
|
|
effectiveDate: formData.positionInfo.positionEffectiveDate || new Date().toISOString().split('T')[0],
|
|
positionDesc: formData.responsibilities.mainResponsibilities || '',
|
|
positionRemark: formData.responsibilities.positionPurpose || ''
|
|
},
|
|
recruitInfo: {
|
|
minEducation: formData.requirements.education || '',
|
|
skillReq: formData.requirements.basicSkills || '',
|
|
workExperience: formData.requirements.workExperienceReq || '',
|
|
otherReq: formData.requirements.otherRequirements || ''
|
|
}
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(API_BASE_URL + '/positions', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(positionData)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast(result.message || '崗位描述已成功儲存至崗位清單!');
|
|
setTimeout(() => {
|
|
switchModule('positionlist');
|
|
}, 1500);
|
|
} else {
|
|
alert('儲存失敗: ' + (result.error || '未知錯誤'));
|
|
}
|
|
} catch (error) {
|
|
console.error('儲存錯誤:', error);
|
|
alert('儲存失敗: ' + error.message);
|
|
}
|
|
}
|
|
|
|
export function cancelJobDescForm() {
|
|
if (confirm('確定要取消嗎?未保存的資料將會遺失。')) {
|
|
document.getElementById('jobDescForm').reset();
|
|
document.getElementById('jd_mainResponsibilities').value = '1、\n2、\n3、\n4、';
|
|
updatePreview();
|
|
}
|
|
}
|
|
|
|
// ==================== 預覽更新 ====================
|
|
|
|
export function updatePreview() {
|
|
const activeModuleBtn = document.querySelector('.module-btn.active');
|
|
if (!activeModuleBtn) return;
|
|
|
|
const activeModule = activeModuleBtn.dataset.module;
|
|
let data;
|
|
if (activeModule === 'position') {
|
|
data = { module: '崗位基礎資料', ...getPositionFormData() };
|
|
} else if (activeModule === 'job') {
|
|
data = { module: '職務基礎資料', ...getJobFormData() };
|
|
} else {
|
|
data = { module: '崗位描述', ...getJobDescFormData() };
|
|
}
|
|
const previewEl = document.getElementById('jsonPreview');
|
|
if (previewEl) {
|
|
previewEl.textContent = JSON.stringify(data, null, 2);
|
|
}
|
|
}
|
|
|
|
// ==================== Toast 訊息 ====================
|
|
|
|
export function showToast(message) {
|
|
const toast = document.getElementById('toast');
|
|
const toastMessage = document.getElementById('toastMessage');
|
|
if (!toast || !toastMessage) {
|
|
console.warn('Toast elements not found, creating dynamic toast');
|
|
const existingToast = document.querySelector('.toast.dynamic-toast');
|
|
if (existingToast) existingToast.remove();
|
|
|
|
const dynamicToast = document.createElement('div');
|
|
dynamicToast.className = 'toast dynamic-toast show';
|
|
dynamicToast.innerHTML = `
|
|
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
|
<span>${message}</span>
|
|
`;
|
|
document.body.appendChild(dynamicToast);
|
|
setTimeout(() => {
|
|
dynamicToast.classList.remove('show');
|
|
setTimeout(() => dynamicToast.remove(), 300);
|
|
}, 3000);
|
|
return;
|
|
}
|
|
toastMessage.textContent = message;
|
|
toast.classList.add('show');
|
|
setTimeout(() => toast.classList.remove('show'), 3000);
|
|
}
|
|
|
|
// ==================== 模組切換 ====================
|
|
|
|
export function switchModule(moduleName) {
|
|
document.querySelectorAll('.module-btn').forEach(b => {
|
|
b.classList.remove('active', 'job-active', 'desc-active');
|
|
});
|
|
document.querySelectorAll('.module-content').forEach(c => c.classList.remove('active'));
|
|
|
|
const targetBtn = document.querySelector(`.module-btn[data-module="${moduleName}"]`);
|
|
if (targetBtn) {
|
|
targetBtn.classList.add('active');
|
|
if (moduleName === 'job') targetBtn.classList.add('job-active');
|
|
if (moduleName === 'jobdesc') targetBtn.classList.add('desc-active');
|
|
}
|
|
|
|
const targetModule = document.getElementById('module-' + moduleName);
|
|
if (targetModule) {
|
|
targetModule.classList.add('active');
|
|
}
|
|
|
|
if (moduleName === 'positionlist') {
|
|
loadPositionList();
|
|
}
|
|
|
|
updatePreview();
|
|
}
|
|
|
|
// ==================== 崗位清單功能 ====================
|
|
|
|
export async function loadPositionList() {
|
|
try {
|
|
showToast('正在載入崗位清單...');
|
|
|
|
const response = await fetch(API_BASE_URL + '/position-list');
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
positionListData = result.data;
|
|
renderPositionList();
|
|
showToast('已載入 ' + positionListData.length + ' 筆崗位資料');
|
|
} else {
|
|
alert('載入失敗: ' + (result.error || '未知錯誤'));
|
|
positionListData = [];
|
|
renderPositionList();
|
|
}
|
|
} catch (error) {
|
|
console.error('載入錯誤:', error);
|
|
alert('載入失敗: ' + error.message);
|
|
positionListData = [];
|
|
renderPositionList();
|
|
}
|
|
}
|
|
|
|
export function renderPositionList() {
|
|
const tbody = document.getElementById('positionListBody');
|
|
if (!tbody) return;
|
|
|
|
if (positionListData.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="8" style="padding: 40px; text-align: center; color: var(--text-secondary);">沒有資料,請先建立崗位或點擊「載入清單」</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = positionListData.map(item => `
|
|
<tr style="border-bottom: 1px solid #eee;">
|
|
<td style="padding: 12px;">${sanitizeHTML(item.positionCode)}</td>
|
|
<td style="padding: 12px;">${sanitizeHTML(item.positionName)}</td>
|
|
<td style="padding: 12px;">${sanitizeHTML(item.positionCategory || '')}</td>
|
|
<td style="padding: 12px;">${sanitizeHTML(item.positionNature || '')}</td>
|
|
<td style="padding: 12px;">${sanitizeHTML(String(item.headcount || ''))}</td>
|
|
<td style="padding: 12px;">${sanitizeHTML(item.positionLevel || '')}</td>
|
|
<td style="padding: 12px;">${sanitizeHTML(item.effectiveDate || '')}</td>
|
|
<td style="padding: 12px; text-align: center;">
|
|
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="viewPositionDesc('${sanitizeHTML(item.positionCode)}')">檢視</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
export function sortPositionList(column) {
|
|
if (currentSortColumn === column) {
|
|
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
currentSortColumn = column;
|
|
currentSortDirection = 'asc';
|
|
}
|
|
|
|
positionListData.sort((a, b) => {
|
|
let valA = a[column];
|
|
let valB = b[column];
|
|
|
|
if (typeof valA === 'string') {
|
|
valA = valA.toLowerCase();
|
|
valB = valB.toLowerCase();
|
|
}
|
|
|
|
if (valA < valB) return currentSortDirection === 'asc' ? -1 : 1;
|
|
if (valA > valB) return currentSortDirection === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
|
|
document.querySelectorAll('.sort-icon').forEach(icon => icon.textContent = '');
|
|
const currentHeader = document.querySelector(`th[data-sort="${column}"] .sort-icon`);
|
|
if (currentHeader) {
|
|
currentHeader.textContent = currentSortDirection === 'asc' ? ' ^' : ' v';
|
|
}
|
|
|
|
renderPositionList();
|
|
}
|
|
|
|
// ==================== 工具函數 ====================
|
|
|
|
function sanitizeHTML(str) {
|
|
if (str === null || str === undefined) return '';
|
|
const temp = document.createElement('div');
|
|
temp.textContent = str;
|
|
return temp.innerHTML;
|
|
}
|
|
|
|
// 暴露全域函數
|
|
if (typeof window !== 'undefined') {
|
|
window.showToast = showToast;
|
|
window.updatePreview = updatePreview;
|
|
window.switchModule = switchModule;
|
|
window.updateCategoryName = updateCategoryName;
|
|
window.updateNatureName = updateNatureName;
|
|
window.updateJobCategoryName = updateJobCategoryName;
|
|
}
|