712 lines
28 KiB
HTML
712 lines
28 KiB
HTML
{% extends 'ewoooc_base.html' %}
|
||
{% block title %}當日業績報表匯入 - EwoooC{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
.auto-import-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18px;
|
||
}
|
||
|
||
.auto-import-hero {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
gap: 16px;
|
||
align-items: center;
|
||
padding: 22px;
|
||
border: 1px solid var(--momo-border-strong);
|
||
border-radius: 8px;
|
||
background:
|
||
radial-gradient(circle at 16px 16px, rgba(42, 37, 32, 0.11) 1px, transparent 1px),
|
||
linear-gradient(135deg, rgba(247, 239, 227, 0.98), rgba(237, 214, 204, 0.5) 48%, rgba(198, 154, 74, 0.12));
|
||
background-size: 18px 18px, auto;
|
||
box-shadow: var(--momo-shadow-soft);
|
||
}
|
||
|
||
.auto-import-hero h2 {
|
||
margin: 0;
|
||
font-size: clamp(1.45rem, 2vw, 2rem);
|
||
font-weight: 850;
|
||
color: var(--momo-text-strong);
|
||
}
|
||
|
||
.auto-import-hero p {
|
||
margin: 0;
|
||
color: var(--momo-text-muted);
|
||
font-weight: 650;
|
||
}
|
||
|
||
.auto-import-page .card {
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: 8px;
|
||
box-shadow: var(--momo-shadow-soft);
|
||
margin-bottom: 1.5rem;
|
||
background: rgba(255, 253, 248, 0.96);
|
||
}
|
||
|
||
.auto-import-page .card-header {
|
||
background: linear-gradient(135deg, var(--momo-warm-terracotta), var(--momo-warm-copper));
|
||
color: var(--momo-page-inverse);
|
||
border: none;
|
||
border-radius: 8px 8px 0 0 !important;
|
||
padding: 1rem 1.25rem;
|
||
}
|
||
|
||
.auto-import-page .card:nth-of-type(2) .card-header {
|
||
background: linear-gradient(135deg, var(--momo-warm-apricot), var(--momo-warm-caramel));
|
||
}
|
||
|
||
.auto-import-page .card:nth-of-type(3) .card-header {
|
||
background: linear-gradient(135deg, var(--momo-warm-copper), var(--momo-warm-olive));
|
||
}
|
||
|
||
.auto-import-page .card-header h5 {
|
||
margin: 0;
|
||
font-weight: 850;
|
||
}
|
||
|
||
.auto-import-page .btn-primary {
|
||
background: linear-gradient(135deg, var(--momo-warm-terracotta), var(--momo-warm-copper));
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 0.6rem 1.5rem;
|
||
font-weight: 750;
|
||
box-shadow: 0 4px 12px rgba(184, 111, 82, 0.18);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.auto-import-page .btn-primary:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 16px rgba(184, 111, 82, 0.24);
|
||
background: linear-gradient(135deg, var(--momo-warm-copper), var(--momo-warm-terracotta));
|
||
}
|
||
|
||
.auto-import-page .btn-secondary {
|
||
border-radius: 8px;
|
||
padding: 0.6rem 1.5rem;
|
||
font-weight: 750;
|
||
transition: all 0.3s ease;
|
||
background: rgba(255, 244, 232, 0.78);
|
||
border-color: var(--momo-border-light);
|
||
color: var(--momo-text-strong);
|
||
}
|
||
|
||
.auto-import-page .btn-success {
|
||
background: linear-gradient(135deg, var(--momo-warm-olive), var(--momo-warm-amber));
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 0.6rem 1.5rem;
|
||
font-weight: 750;
|
||
box-shadow: 0 4px 12px rgba(143, 137, 104, 0.18);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.auto-import-page .btn-success:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 16px rgba(143, 137, 104, 0.24);
|
||
background: linear-gradient(135deg, var(--momo-warm-amber), var(--momo-warm-olive));
|
||
}
|
||
|
||
.auto-import-page .table {
|
||
background: var(--momo-bg-elevated);
|
||
width: 100%;
|
||
}
|
||
|
||
.auto-import-page .table thead th {
|
||
background: linear-gradient(90deg, var(--momo-warm-copper), var(--momo-warm-terracotta));
|
||
border-bottom: 0;
|
||
color: var(--momo-page-inverse);
|
||
font-weight: 800;
|
||
text-transform: uppercase;
|
||
font-size: 0.75rem;
|
||
letter-spacing: 0.5px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.auto-import-page .table tbody td {
|
||
vertical-align: middle;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.auto-import-page .table-responsive {
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
/* 錯誤訊息可換行 */
|
||
.auto-import-page .table tbody td.error-cell {
|
||
white-space: normal;
|
||
max-width: 250px;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.auto-import-page .badge {
|
||
padding: 0.4rem 0.8rem;
|
||
font-weight: 500;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.auto-import-page .badge-pending {
|
||
background: var(--momo-warm-honey-soft);
|
||
color: #6f4c08;
|
||
}
|
||
|
||
.auto-import-page .badge-downloading {
|
||
background: var(--momo-warm-peach-soft);
|
||
color: #5f4035;
|
||
}
|
||
|
||
.auto-import-page .badge-importing {
|
||
background: var(--momo-warm-mahogany-soft);
|
||
color: #5f4035;
|
||
}
|
||
|
||
.auto-import-page .badge-completed {
|
||
background: var(--momo-warm-earth-soft);
|
||
color: #4a512d;
|
||
}
|
||
|
||
.auto-import-page .badge-failed {
|
||
background: var(--momo-warm-rust-soft);
|
||
color: #823c43;
|
||
}
|
||
|
||
.auto-import-page .progress {
|
||
height: 8px;
|
||
border-radius: 4px;
|
||
background: rgba(42, 37, 32, 0.08);
|
||
}
|
||
|
||
.auto-import-page .progress-bar {
|
||
background: linear-gradient(90deg, var(--momo-warm-terracotta), var(--momo-warm-apricot), var(--momo-warm-amber));
|
||
}
|
||
|
||
.auto-import-page .form-label {
|
||
font-weight: 750;
|
||
color: var(--momo-text-primary);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.auto-import-page .form-control:focus {
|
||
border-color: var(--momo-warm-terracotta);
|
||
box-shadow: 0 0 0 0.2rem rgba(184, 111, 82, 0.15);
|
||
}
|
||
|
||
.auto-import-page .alert {
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.auto-import-page .alert-info {
|
||
color: #5f4035;
|
||
background:
|
||
linear-gradient(135deg, rgba(237, 214, 204, 0.58), rgba(247, 239, 227, 0.88));
|
||
border-color: rgba(184, 111, 82, 0.22);
|
||
}
|
||
|
||
.auto-import-page .alert-light {
|
||
color: var(--momo-text-secondary);
|
||
background: rgba(255, 248, 239, 0.82);
|
||
border-color: rgba(216, 193, 170, 0.72) !important;
|
||
}
|
||
|
||
.auto-import-page .empty-state {
|
||
text-align: center;
|
||
padding: 3rem 1rem;
|
||
color: var(--momo-text-muted);
|
||
}
|
||
|
||
.auto-import-page .empty-state i {
|
||
font-size: 3rem;
|
||
margin-bottom: 1rem;
|
||
opacity: 0.3;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block ewooo_content %}
|
||
<div class="auto-import-page">
|
||
<!-- 頁面標題 -->
|
||
<section class="auto-import-hero">
|
||
<div>
|
||
<h2><i class="fas fa-cloud-download-alt me-2"></i>當日業績報表匯入</h2>
|
||
<p>
|
||
<i class="fas fa-info-circle me-1"></i>
|
||
支援兩種匯入方式:<strong>Google Drive 自動匯入</strong>(每 30 分鐘檢查)或 <strong>手動上傳</strong>
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 配置區 -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h5><i class="fas fa-cog me-2"></i>Google Drive 自動匯入配置</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="configAlert"></div>
|
||
|
||
<div class="alert alert-light border">
|
||
<p class="mb-0 small">
|
||
<i class="fas fa-sync-alt me-1"></i>
|
||
系統每 30 分鐘自動檢查 Google Drive → 下載檔案 → 匯入資料庫 → 刪除雲端原檔
|
||
</p>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<label for="folderPath" class="form-label">Google Drive 資料夾路徑</label>
|
||
<input type="text" class="form-control" id="folderPath" placeholder="例如: 業績報表/當日業績">
|
||
<small class="text-muted">設定要監控的 Google Drive 資料夾路徑</small>
|
||
</div>
|
||
<div class="col-md-6 mb-3">
|
||
<label for="filePattern" class="form-label">檔案名稱模式(選填)</label>
|
||
<input type="text" class="form-control" id="filePattern" placeholder="例如: 即時業績_當日">
|
||
<small class="text-muted">用於過濾特定名稱的檔案</small>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="d-flex flex-wrap gap-2">
|
||
<button class="btn btn-primary" onclick="saveConfig()">
|
||
<i class="fas fa-save me-1"></i>儲存配置
|
||
</button>
|
||
<button class="btn btn-secondary" onclick="testConnection()">
|
||
<i class="fas fa-plug me-1"></i>測試連接
|
||
</button>
|
||
<button class="btn btn-secondary" onclick="listFiles()">
|
||
<i class="fas fa-list me-1"></i>列出檔案
|
||
</button>
|
||
<button class="btn btn-success" onclick="manualImport()">
|
||
<i class="fas fa-play me-1"></i>立即匯入
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 手動上傳區 -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h5><i class="fas fa-upload me-2"></i>手動上傳匯入</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="uploadAlert"></div>
|
||
|
||
<div class="alert alert-info">
|
||
<h6 class="fw-bold mb-2"><i class="fas fa-info-circle me-2"></i>每日業績快照</h6>
|
||
<p class="mb-1">匯入格式:<code>即時業績_當日_YYYYMMDD.xlsx</code>(例如:即時業績_當日_20260113.xlsx)</p>
|
||
<p class="mb-0 small">資料將會<strong>累加寫入</strong>至 <code>daily_sales_snapshot</code> 資料表,並自動去重。</p>
|
||
</div>
|
||
|
||
<div class="row align-items-end">
|
||
<div class="col-md-8 mb-3 mb-md-0">
|
||
<label for="manualUploadFile" class="form-label">選擇檔案</label>
|
||
<input type="file" class="form-control" id="manualUploadFile" accept=".xlsx,.xls">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<button class="btn btn-primary w-100" onclick="uploadManualFile()">
|
||
<i class="fas fa-upload me-1"></i>上傳並匯入
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 匯入任務清單 -->
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5 class="mb-0"><i class="fas fa-tasks me-2"></i>匯入任務歷史</h5>
|
||
<button class="btn btn-warning btn-sm" onclick="resetStuckJobs()">
|
||
<i class="fas fa-redo me-1"></i>重置卡住的任務
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="jobsLoading" class="text-center py-4" style="display: none;">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">載入中...</span>
|
||
</div>
|
||
<p class="mt-2 text-muted">載入中...</p>
|
||
</div>
|
||
|
||
<div id="jobsEmpty" class="empty-state" style="display: none;">
|
||
<i class="fas fa-inbox"></i>
|
||
<p>尚無匯入記錄</p>
|
||
</div>
|
||
|
||
<div id="jobsTableContainer" style="display: none;">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover align-middle" id="jobsTable">
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>檔案名稱</th>
|
||
<th>狀態</th>
|
||
<th>進度</th>
|
||
<th>成功/總</th>
|
||
<th>開始時間</th>
|
||
<th>完成時間</th>
|
||
<th>錯誤訊息</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="jobsTableBody">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
// 載入配置
|
||
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);
|
||
// 使用 DOM API 建構元素,避免 XSS(禁止直接 innerHTML 插入 message)
|
||
alertDiv.innerHTML = '';
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = `alert alert-${type} alert-dismissible fade show`;
|
||
wrapper.setAttribute('role', 'alert');
|
||
const msgNode = document.createTextNode(message);
|
||
wrapper.appendChild(msgNode);
|
||
const closeBtn = document.createElement('button');
|
||
closeBtn.type = 'button';
|
||
closeBtn.className = 'btn-close';
|
||
closeBtn.setAttribute('data-bs-dismiss', 'alert');
|
||
closeBtn.setAttribute('aria-label', 'Close');
|
||
wrapper.appendChild(closeBtn);
|
||
alertDiv.appendChild(wrapper);
|
||
|
||
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);
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %}
|