Files
hr-position-system/js/csv.js
DonaldFang 方士碩 a6af297623 backup: 完成 HR_position_ 表格前綴重命名與欄位對照表整理
變更內容:
- 所有資料表加上 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>
2025-12-09 12:05:20 +08:00

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;
}