350 lines
14 KiB
JavaScript
350 lines
14 KiB
JavaScript
/* page-auto-import.js — Turn C
|
||
* 抽自 auto_import_index.html 原 inline <script>
|
||
* 邏輯不變:Google Drive 設定 / 手動上傳 / 任務輪詢
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
// 載入配置
|
||
async function loadConfig() {
|
||
try {
|
||
const response = await fetch('/api/import_config');
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
document.getElementById('folderPath').value = result.data.folder_path || '';
|
||
document.getElementById('filePattern').value = result.data.file_pattern || '';
|
||
}
|
||
} catch (error) {
|
||
console.error('載入配置失敗:', error);
|
||
}
|
||
}
|
||
|
||
// 儲存配置
|
||
async function saveConfig() {
|
||
const folderPath = document.getElementById('folderPath').value;
|
||
const filePattern = document.getElementById('filePattern').value;
|
||
|
||
try {
|
||
const response = await fetch('/api/import_config', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
folder_path: folderPath,
|
||
file_pattern: filePattern
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showAlert('configAlert', 'success', '<i class="fas fa-check-circle me-2"></i>配置已儲存');
|
||
} else {
|
||
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>' + result.message);
|
||
}
|
||
} catch (error) {
|
||
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>儲存配置失敗: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 測試連接
|
||
async function testConnection() {
|
||
showAlert('configAlert', 'info', '<i class="fas fa-spinner fa-spin me-2"></i>正在測試 Google Drive 連接...');
|
||
|
||
try {
|
||
const response = await fetch('/api/test_drive_connection', {
|
||
method: 'POST'
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showAlert('configAlert', 'success', '<i class="fas fa-check-circle me-2"></i>' + result.message);
|
||
} else {
|
||
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>' + result.message);
|
||
}
|
||
} catch (error) {
|
||
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>測試失敗: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 列出檔案
|
||
async function listFiles() {
|
||
const folderPath = document.getElementById('folderPath').value;
|
||
const filePattern = document.getElementById('filePattern').value;
|
||
|
||
showAlert('configAlert', 'info', '<i class="fas fa-spinner fa-spin me-2"></i>正在列出 Google Drive 檔案...');
|
||
|
||
try {
|
||
const response = await fetch('/api/list_drive_files', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
folder_path: folderPath,
|
||
file_pattern: filePattern
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
const fileList = result.data.map(f => `<li class="mb-1"><i class="fas fa-file-excel me-1"></i>${f.name}</li>`).join('');
|
||
showAlert('configAlert', 'success', `<i class="fas fa-check-circle me-2"></i>找到 ${result.count} 個檔案:<ul class="mt-2 mb-0">${fileList || '<li>無</li>'}</ul>`);
|
||
} else {
|
||
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>' + result.message);
|
||
}
|
||
} catch (error) {
|
||
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>列出檔案失敗: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 手動匯入
|
||
async function manualImport() {
|
||
if (!confirm('確定要立即執行匯入嗎?')) {
|
||
return;
|
||
}
|
||
|
||
showAlert('configAlert', 'info', '<i class="fas fa-spinner fa-spin me-2"></i>正在執行匯入,請稍候...');
|
||
|
||
try {
|
||
const response = await fetch('/api/manual_import', {
|
||
method: 'POST'
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showAlert('configAlert', 'success', '<i class="fas fa-check-circle me-2"></i>' + result.message);
|
||
setTimeout(() => loadJobs(), 1000);
|
||
} else {
|
||
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>' + result.message);
|
||
}
|
||
} catch (error) {
|
||
showAlert('configAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>匯入失敗: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 載入任務列表
|
||
async function loadJobs() {
|
||
document.getElementById('jobsLoading').style.display = 'block';
|
||
document.getElementById('jobsEmpty').style.display = 'none';
|
||
document.getElementById('jobsTableContainer').style.display = 'none';
|
||
|
||
try {
|
||
const response = await fetch('/api/import_jobs?limit=50');
|
||
const result = await response.json();
|
||
|
||
document.getElementById('jobsLoading').style.display = 'none';
|
||
|
||
if (result.success && result.data.length > 0) {
|
||
document.getElementById('jobsTableContainer').style.display = 'block';
|
||
renderJobs(result.data);
|
||
} else {
|
||
document.getElementById('jobsEmpty').style.display = 'block';
|
||
}
|
||
} catch (error) {
|
||
document.getElementById('jobsLoading').style.display = 'none';
|
||
document.getElementById('jobsEmpty').style.display = 'block';
|
||
console.error('載入任務列表失敗:', error);
|
||
}
|
||
}
|
||
|
||
// 渲染任務列表
|
||
function renderJobs(jobs) {
|
||
const tbody = document.getElementById('jobsTableBody');
|
||
tbody.innerHTML = '';
|
||
|
||
jobs.forEach(job => {
|
||
const tr = document.createElement('tr');
|
||
const fileName = job.drive_file_name || 'N/A';
|
||
const errorMsg = job.error_message || '-';
|
||
|
||
tr.innerHTML = `
|
||
<td class="fw-bold">#${job.id}</td>
|
||
<td>
|
||
<i class="fas fa-file-excel text-success me-1"></i>${escapeHtml(fileName)}
|
||
</td>
|
||
<td>
|
||
<span class="badge badge-${job.status}">${getStatusText(job.status)}</span>
|
||
</td>
|
||
<td style="min-width: 150px;">
|
||
<div class="progress mb-1">
|
||
<div class="progress-bar" role="progressbar" style="width: ${job.progress_percent}%"
|
||
aria-valuenow="${job.progress_percent}" aria-valuemin="0" aria-valuemax="100">
|
||
</div>
|
||
</div>
|
||
<small class="text-muted">${Math.round(job.progress_percent)}% - ${escapeHtml(job.current_step || '')}</small>
|
||
</td>
|
||
<td class="text-center">
|
||
<span class="badge bg-success">${job.success_rows || 0}</span> /
|
||
<span class="badge bg-secondary">${job.total_rows || 0}</span>
|
||
</td>
|
||
<td><small>${formatTime(job.started_at)}</small></td>
|
||
<td><small>${formatTime(job.completed_at)}</small></td>
|
||
<td class="error-cell"><small class="text-danger">${escapeHtml(errorMsg)}</small></td>
|
||
<td class="text-center">
|
||
${(job.status === 'downloading' || job.status === 'importing')
|
||
? `<button class="btn btn-sm btn-outline-danger" onclick="failJob(${job.id})">
|
||
<i class="fas fa-times"></i>
|
||
</button>`
|
||
: '-'}
|
||
</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
// 轉義 HTML 特殊字元
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// 取得狀態文字
|
||
function getStatusText(status) {
|
||
const statusMap = {
|
||
'pending': '⏳ 等待中',
|
||
'downloading': '⬇️ 下載中',
|
||
'importing': '📥 匯入中',
|
||
'completed': '✅ 已完成',
|
||
'failed': '❌ 失敗'
|
||
};
|
||
return statusMap[status] || status;
|
||
}
|
||
|
||
// 格式化時間
|
||
function formatTime(timeStr) {
|
||
if (!timeStr) return '-';
|
||
const date = new Date(timeStr);
|
||
return date.toLocaleString('zh-TW', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
|
||
// 顯示提示
|
||
function showAlert(elementId, type, message) {
|
||
const alertDiv = document.getElementById(elementId);
|
||
alertDiv.innerHTML = `
|
||
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
|
||
${message}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||
</div>
|
||
`;
|
||
|
||
if (type === 'success' || type === 'danger') {
|
||
setTimeout(() => {
|
||
const alert = alertDiv.querySelector('.alert');
|
||
if (alert) {
|
||
const bsAlert = new bootstrap.Alert(alert);
|
||
bsAlert.close();
|
||
}
|
||
}, 5000);
|
||
}
|
||
}
|
||
|
||
// 手動上傳檔案
|
||
async function uploadManualFile() {
|
||
const fileInput = document.getElementById('manualUploadFile');
|
||
const file = fileInput.files[0];
|
||
|
||
if (!file) {
|
||
showAlert('uploadAlert', 'warning', '<i class="fas fa-exclamation-triangle me-2"></i>請先選擇檔案');
|
||
return;
|
||
}
|
||
|
||
// 檔名驗證
|
||
if (!file.name.includes('即時業績') || !file.name.includes('當日')) {
|
||
if (!confirm('檔名似乎不符合格式(應包含「即時業績」和「當日」),確定要繼續嗎?')) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
showAlert('uploadAlert', 'info', '<i class="fas fa-spinner fa-spin me-2"></i>正在上傳並匯入檔案,請稍候...');
|
||
|
||
try {
|
||
const response = await fetch('/api/import_excel', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.status === 'success') {
|
||
showAlert('uploadAlert', 'success',
|
||
`<i class="fas fa-check-circle me-2"></i>${result.message}<br>` +
|
||
`<small>資料表: ${result.table} | 共 ${result.rows} 筆資料</small>`
|
||
);
|
||
fileInput.value = '';
|
||
setTimeout(() => loadJobs(), 1000);
|
||
} else {
|
||
showAlert('uploadAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>' + result.message);
|
||
}
|
||
} catch (error) {
|
||
showAlert('uploadAlert', 'danger', '<i class="fas fa-exclamation-circle me-2"></i>上傳失敗: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 初始化
|
||
loadConfig();
|
||
loadJobs();
|
||
|
||
// 每 10 秒自動刷新任務列表
|
||
setInterval(loadJobs, 10000);
|
||
|
||
// 重置卡住的任務
|
||
async function resetStuckJobs() {
|
||
if (!confirm('確定要重置所有卡住超過 1 小時的任務嗎?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/reset_stuck_jobs', { method: 'POST' });
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert(result.message);
|
||
loadJobs();
|
||
} else {
|
||
alert('重置失敗: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
alert('重置失敗: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 手動將任務標記為失敗
|
||
async function failJob(jobId) {
|
||
if (!confirm(`確定要取消任務 #${jobId} 嗎?`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/import_jobs/${jobId}/fail`, { method: 'POST' });
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert(result.message);
|
||
loadJobs();
|
||
} else {
|
||
alert('取消失敗: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
alert('取消失敗: ' + error.message);
|
||
}
|
||
}
|
||
|
||
})();
|