變更內容: - 所有資料表加上 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>
338 lines
11 KiB
JavaScript
338 lines
11 KiB
JavaScript
/**
|
|
* CSV 匯入匯出模組
|
|
* 處理各表單的 CSV 資料交換
|
|
*/
|
|
|
|
const API_BASE_URL = '/api';
|
|
|
|
// ==================== CSV 工具函數 ====================
|
|
|
|
export const CSVUtils = {
|
|
/**
|
|
* 匯出資料到 CSV
|
|
*/
|
|
exportToCSV(data, filename, headers) {
|
|
if (!data || data.length === 0) {
|
|
console.warn('No data to export');
|
|
return;
|
|
}
|
|
|
|
const csvHeaders = headers || Object.keys(data[0]);
|
|
const csvRows = data.map(row => {
|
|
return csvHeaders.map(header => {
|
|
let value = row[header] !== undefined ? row[header] : '';
|
|
// 處理包含逗號或換行的值
|
|
if (typeof value === 'string' && (value.includes(',') || value.includes('\n') || value.includes('"'))) {
|
|
value = '"' + value.replace(/"/g, '""') + '"';
|
|
}
|
|
return value;
|
|
}).join(',');
|
|
});
|
|
|
|
const csvContent = '\uFEFF' + [csvHeaders.join(','), ...csvRows].join('\n'); // BOM for UTF-8
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = filename;
|
|
link.click();
|
|
},
|
|
|
|
/**
|
|
* 從 CSV 匯入資料
|
|
*/
|
|
importFromCSV(file, callback) {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
const text = e.target.result;
|
|
const lines = text.split('\n').filter(line => line.trim());
|
|
if (lines.length < 2) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
const headers = this.parseCSVLine(lines[0]);
|
|
const data = [];
|
|
|
|
for (let i = 1; i < lines.length; i++) {
|
|
const values = this.parseCSVLine(lines[i]);
|
|
const row = {};
|
|
headers.forEach((header, index) => {
|
|
row[header.trim()] = values[index] ? values[index].trim() : '';
|
|
});
|
|
data.push(row);
|
|
}
|
|
|
|
callback(data);
|
|
};
|
|
reader.readAsText(file, 'UTF-8');
|
|
},
|
|
|
|
/**
|
|
* 解析 CSV 行(處理引號內的逗號)
|
|
*/
|
|
parseCSVLine(line) {
|
|
const result = [];
|
|
let current = '';
|
|
let inQuotes = false;
|
|
|
|
for (let i = 0; i < line.length; i++) {
|
|
const char = line[i];
|
|
if (char === '"') {
|
|
if (inQuotes && line[i + 1] === '"') {
|
|
current += '"';
|
|
i++;
|
|
} else {
|
|
inQuotes = !inQuotes;
|
|
}
|
|
} else if (char === ',' && !inQuotes) {
|
|
result.push(current);
|
|
current = '';
|
|
} else {
|
|
current += char;
|
|
}
|
|
}
|
|
result.push(current);
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// ==================== 崗位資料 CSV ====================
|
|
|
|
export function downloadPositionCSVTemplate() {
|
|
window.location.href = API_BASE_URL + '/positions/csv-template';
|
|
if (typeof showToast === 'function') showToast('正在下載崗位資料範本...');
|
|
}
|
|
|
|
export 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);
|
|
if (typeof showToast === 'function') showToast('崗位資料已匯出!');
|
|
}
|
|
|
|
export function importPositionsCSV() {
|
|
document.getElementById('positionCSVInput').click();
|
|
}
|
|
|
|
export function handlePositionCSVImport(event) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
if (typeof showToast === 'function') showToast('正在匯入崗位資料...');
|
|
|
|
fetch(API_BASE_URL + '/positions/import-csv', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
let message = data.message;
|
|
if (data.errors && data.errors.length > 0) {
|
|
message += '\n\n錯誤詳情:\n' + data.errors.join('\n');
|
|
}
|
|
alert(message);
|
|
} else {
|
|
alert('匯入失敗: ' + (data.error || '未知錯誤'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('匯入錯誤:', error);
|
|
alert('匯入失敗: ' + error.message);
|
|
})
|
|
.finally(() => {
|
|
event.target.value = '';
|
|
});
|
|
}
|
|
|
|
// ==================== 職務資料 CSV ====================
|
|
|
|
export function downloadJobCSVTemplate() {
|
|
window.location.href = API_BASE_URL + '/jobs/csv-template';
|
|
if (typeof showToast === 'function') showToast('正在下載職務資料範本...');
|
|
}
|
|
|
|
export 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('job_hasAttBonus')?.checked,
|
|
hasHousingAllowance: document.getElementById('job_hasHouseAllow')?.checked
|
|
}];
|
|
|
|
const headers = ['jobCategoryCode', 'jobCategoryName', 'jobCode', 'jobName', 'jobNameEn',
|
|
'jobEffectiveDate', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel',
|
|
'hasAttendanceBonus', 'hasHousingAllowance'];
|
|
|
|
CSVUtils.exportToCSV(data, 'jobs.csv', headers);
|
|
if (typeof showToast === 'function') showToast('職務資料已匯出!');
|
|
}
|
|
|
|
export function importJobsCSV() {
|
|
document.getElementById('jobCSVInput').click();
|
|
}
|
|
|
|
export function handleJobCSVImport(event) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
if (typeof showToast === 'function') showToast('正在匯入職務資料...');
|
|
|
|
fetch(API_BASE_URL + '/jobs/import-csv', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
let message = data.message;
|
|
if (data.errors && data.errors.length > 0) {
|
|
message += '\n\n錯誤詳情:\n' + data.errors.join('\n');
|
|
}
|
|
alert(message);
|
|
} else {
|
|
alert('匯入失敗: ' + (data.error || '未知錯誤'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('匯入錯誤:', error);
|
|
alert('匯入失敗: ' + error.message);
|
|
})
|
|
.finally(() => {
|
|
event.target.value = '';
|
|
});
|
|
}
|
|
|
|
// ==================== 崗位描述 CSV ====================
|
|
|
|
export 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);
|
|
if (typeof showToast === 'function') showToast('崗位描述已匯出!');
|
|
}
|
|
|
|
export function importDescriptionsCSV() {
|
|
document.getElementById('descCSVInput').click();
|
|
}
|
|
|
|
export 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];
|
|
}
|
|
});
|
|
if (typeof showToast === 'function') {
|
|
showToast(`已匯入 ${data.length} 筆崗位描述資料(顯示第一筆)`);
|
|
}
|
|
}
|
|
});
|
|
event.target.value = '';
|
|
}
|
|
|
|
// ==================== 崗位清單 CSV ====================
|
|
|
|
export function exportPositionListCSV(positionListData) {
|
|
if (!positionListData || positionListData.length === 0) {
|
|
if (typeof showToast === 'function') showToast('請先載入清單資料');
|
|
return;
|
|
}
|
|
const headers = ['positionCode', 'positionName', 'businessUnit', 'department', 'positionCategory', 'headcount', 'effectiveDate'];
|
|
CSVUtils.exportToCSV(positionListData, 'position_list.csv', headers);
|
|
if (typeof showToast === 'function') showToast('崗位清單已匯出!');
|
|
}
|
|
|
|
// ==================== 部門職責 CSV ====================
|
|
|
|
export function importDeptFunctionCSV() {
|
|
document.getElementById('deptFunctionCsvInput').click();
|
|
}
|
|
|
|
export function handleDeptFunctionCSVImport(event, callback) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
CSVUtils.importFromCSV(file, (data) => {
|
|
if (data && data.length > 0) {
|
|
const row = data[0];
|
|
Object.keys(row).forEach(key => {
|
|
const el = document.getElementById(key);
|
|
if (el) el.value = row[key];
|
|
});
|
|
if (typeof showToast === 'function') showToast('已匯入 CSV 資料!');
|
|
if (callback) callback(data);
|
|
}
|
|
});
|
|
event.target.value = '';
|
|
}
|
|
|
|
export function exportDeptFunctionCSV(formData) {
|
|
const headers = Object.keys(formData);
|
|
CSVUtils.exportToCSV([formData], 'dept_function.csv', headers);
|
|
if (typeof showToast === 'function') showToast('部門職責資料已匯出!');
|
|
}
|
|
|
|
// ==================== 工具函數 ====================
|
|
|
|
function getFieldValue(elementId) {
|
|
const el = document.getElementById(elementId);
|
|
return el ? el.value.trim() : '';
|
|
}
|
|
|
|
// 暴露到全域
|
|
if (typeof window !== 'undefined') {
|
|
window.CSVUtils = CSVUtils;
|
|
}
|