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>
This commit is contained in:
337
js/csv.js
Normal file
337
js/csv.js
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user