// DOM Elements const elements = { uploadArea: document.getElementById('uploadArea'), fileInput: document.getElementById('fileInput'), loadingOverlay: document.getElementById('loadingOverlay'), loadingText: document.getElementById('loadingText'), noData: document.getElementById('noData'), dataDisplay: document.getElementById('dataDisplay'), uploadModal: document.getElementById('uploadModal'), fileList: document.getElementById('fileList'), fileSelector: document.getElementById('fileSelector'), dailyData: document.getElementById('dailyData'), searchInput: document.getElementById('searchInput'), // Stats totalAmount: document.getElementById('totalAmount'), totalQuantity: document.getElementById('totalQuantity'), totalDays: document.getElementById('totalDays'), totalProducts: document.getElementById('totalProducts') }; // State let state = { allData: null, filteredData: null, currentFilter: 'all', searchTerm: '', currentFile: null, fileList: [], currentPage: 1, itemsPerPage: 50 }; // --- Event Listeners --- document.addEventListener('DOMContentLoaded', () => { loadFileList(); if (elements.searchInput) { elements.searchInput.addEventListener('input', (e) => { state.searchTerm = e.target.value.toLowerCase(); applyFilters(); }); } }); if (elements.fileInput) { elements.fileInput.addEventListener('change', (e) => handleFile(e.target.files[0])); } if (elements.uploadArea) { elements.uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); elements.uploadArea.classList.add('active'); }); elements.uploadArea.addEventListener('dragleave', () => { elements.uploadArea.classList.remove('active'); }); elements.uploadArea.addEventListener('drop', (e) => { e.preventDefault(); elements.uploadArea.classList.remove('active'); if (e.dataTransfer.files.length > 0) { handleFile(e.dataTransfer.files[0]); } }); elements.uploadArea.addEventListener('click', () => { elements.fileInput.click(); }); } // --- Core Functions --- function handleFile(file) { if (!file) return; if (!file.name.match(/\.(xlsx|xls)$/i)) { alert('请选择 Excel 文件 (.xlsx 或 .xls)'); return; } closeUploadModal(); uploadFile(file); } function uploadFile(file) { const formData = new FormData(); formData.append('file', file); showLoading(true, '正在上传并分析...'); fetch('/upload', { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { setTimeout(() => { showLoading(false); if (data.success) { state.currentFile = data.filename; displayData(data.data); loadFileList(); } else { alert(data.error || '上传失败'); } }, 500); }) .catch(err => { showLoading(false); alert('上传错误: ' + err.message); }); } function loadFileList() { fetch('/files') .then(res => res.json()) .then(data => { if (data.files && data.files.length > 0) { state.fileList = data.files; renderFileList(data.files); if (!state.currentFile) { loadFile(data.files[0].filename); } } else { elements.fileSelector.style.display = 'none'; } }); } function loadFile(filename) { showLoading(true, '加载数据中...'); fetch(`/load/${filename}`) .then(res => res.json()) .then(data => { setTimeout(() => { showLoading(false); if (data.success) { state.currentFile = filename; displayData(data.data); renderFileList(state.fileList); // Update active state } else { elements.noData.style.display = 'flex'; elements.dataDisplay.style.display = 'none'; } }, 300); }) .catch(() => { showLoading(false); elements.noData.style.display = 'flex'; }); } function displayData(data) { state.allData = data; elements.noData.style.display = 'none'; elements.dataDisplay.style.display = 'block'; applyFilters(); } function renderStats(data) { let totalQty = 0; let totalAmt = 0; // 1. Calculate Unique Days (based on YYYY-MM-DD) const uniqueDates = new Set(); // 2. Track Unique Products const uniqueProducts = new Set(); data.daily_summary.forEach(day => { // Extract YYYY-MM-DD from timestamp "YYYY-MM-DD HH:MM:SS" if (day.date && day.date.length >= 10) { uniqueDates.add(day.date.substring(0, 10)); } // Sum up totals // Priority: Use the sum of products (day.total_...) if available. // If products are missing/zero but header info (summary_info) exists, use that. if (day.products && day.products.length > 0) { // Use calculated sum from products totalQty += day.total_quantity; totalAmt += day.total_amount; } else if (day.summary_info) { // Fallback to header info if no products parsed totalQty += day.summary_info.total_quantity; totalAmt += day.summary_info.total_amount; } else { totalQty += day.total_quantity; totalAmt += day.total_amount; } // Collect unique products if (day.products) { day.products.forEach(p => uniqueProducts.add(p.product)); } }); // Count Animation animateValue(elements.totalAmount, totalAmt, '¥'); animateValue(elements.totalQuantity, totalQty, ''); animateValue(elements.totalDays, uniqueDates.size, ''); // Use unique dates count animateValue(elements.totalProducts, uniqueProducts.size, ''); } function animateValue(obj, end, prefix = '') { if (!obj) return; let startTimestamp = null; const duration = 1000; const start = 0; const step = (timestamp) => { if (!startTimestamp) startTimestamp = timestamp; const progress = Math.min((timestamp - startTimestamp) / duration, 1); const value = Math.floor(progress * (end - start) + start); if (prefix === '¥') { obj.innerHTML = prefix + (progress * end).toFixed(2); } else { obj.innerHTML = prefix + value; } if (progress < 1) { window.requestAnimationFrame(step); } else { if (prefix === '¥') { obj.innerHTML = prefix + end.toFixed(2); } else { obj.innerHTML = prefix + end; } } }; window.requestAnimationFrame(step); } // Pagination State const ITEMS_PER_PAGE = 20; let displayedItems = ITEMS_PER_PAGE; function renderDailyList(data) { const listContainer = elements.dailyData; const totalItems = data.daily_summary.length; // Slice data based on current limit const visibleData = data.daily_summary.slice(0, displayedItems); // Render list const listHTML = visibleData.map(day => { const totalAmt = day.summary_info ? day.summary_info.total_amount : day.total_amount; const totalQty = day.summary_info ? day.summary_info.total_quantity : day.total_quantity; return `
${day.date} ¥${totalAmt.toFixed(2)} / ${totalQty}件
${day.products.map(p => `
${p.product}
¥${p.price.toFixed(2)} x${p.quantity} ¥${p.amount.toFixed(2)}
`).join('')}
`; }).join(''); // Append "Show More" button if needed let buttonHTML = ''; if (displayedItems < totalItems) { buttonHTML = `
`; } listContainer.innerHTML = listHTML + buttonHTML; } function loadMoreItems() { displayedItems += ITEMS_PER_PAGE; if (state.filteredData) { renderDailyList(state.filteredData); } else if (state.allData) { renderDailyList(state.allData); // Fallback if no filter active } } function toggleDayDetails(header) { const card = header.parentElement; card.classList.toggle('expanded'); } function applyFilters() { if (!state.allData) return; // Reset pagination on filter change displayedItems = ITEMS_PER_PAGE; const searchTerm = (state.searchTerm || "").trim(); const currentFilter = state.currentFilter; // Filter without cloning for massive performance boost const filteredItems = state.allData.daily_summary.map(day => { // 1. Search filter let matchedProducts = day.products; if (searchTerm) { matchedProducts = day.products.filter(p => p.product.toLowerCase().includes(searchTerm) ); } if (matchedProducts.length === 0) return null; // 2. Amount filter const amt = day.summary_info ? day.summary_info.total_amount : day.total_amount; let amountMatch = true; if (currentFilter === 'high') amountMatch = (amt >= 1000); else if (currentFilter === 'medium') amountMatch = (amt >= 100 && amt < 1000); else if (currentFilter === 'low') amountMatch = (amt < 100); if (amountMatch) { return { ...day, products: matchedProducts, total_quantity: searchTerm ? matchedProducts.reduce((a, b) => a + b.quantity, 0) : day.total_quantity, total_amount: searchTerm ? matchedProducts.reduce((a, b) => a + b.amount, 0) : day.total_amount, summary_info: searchTerm ? null : day.summary_info }; } return null; }).filter(d => d); state.filteredData = { daily_summary: filteredItems }; renderStats(state.filteredData); renderDailyList(state.filteredData); } function filterByAmount(type, btnElement) { state.currentFilter = type; document.querySelectorAll('.filter-chip').forEach(btn => btn.classList.remove('active')); if (btnElement) { btnElement.classList.add('active'); } applyFilters(); } function renderFileList(files) { elements.fileSelector.style.display = 'flex'; elements.fileList.innerHTML = files.map(f => `
${f.original_name}
`).join(''); } function deleteFile(filename, event) { if (event) event.stopPropagation(); if (!confirm('确认删除此文件?')) return; fetch(`/delete/${filename}`, { method: 'POST' }) .then(res => res.json()) .then(data => { if (data.success) { if (state.currentFile === filename) { state.currentFile = null; state.allData = null; state.filteredData = null; elements.noData.style.display = 'flex'; elements.dataDisplay.style.display = 'none'; } loadFileList(); } else { alert(data.error || '删除失败'); } }) .catch(err => alert('删除错误: ' + err.message)); } // --- Utils --- function openUploadModal() { elements.uploadModal.classList.add('active'); } function closeUploadModal() { elements.uploadModal.classList.remove('active'); } // --- Auto Download --- let autoDownloadPollTimer = null; function openAutoDownloadModal() { const modal = document.getElementById('autoDownloadModal'); // 默认日期为昨天 const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const dateStr = yesterday.toISOString().split('T')[0]; document.getElementById('autoStartDate').value = dateStr; document.getElementById('autoEndDate').value = dateStr; document.getElementById('autoDownloadStatus').style.display = 'none'; document.getElementById('autoDownloadBtn').disabled = false; modal.classList.add('active'); } function closeAutoDownloadModal() { const modal = document.getElementById('autoDownloadModal'); modal.classList.remove('active'); if (autoDownloadPollTimer) { clearInterval(autoDownloadPollTimer); autoDownloadPollTimer = null; } } function startAutoDownload() { const startDate = document.getElementById('autoStartDate').value; const endDate = document.getElementById('autoEndDate').value; if (!startDate) { alert('请选择开始日期'); return; } const btn = document.getElementById('autoDownloadBtn'); btn.disabled = true; btn.innerHTML = ' 下载中...'; const statusDiv = document.getElementById('autoDownloadStatus'); const statusText = document.getElementById('autoDownloadStatusText'); statusDiv.style.display = 'flex'; statusText.textContent = '正在启动下载任务...'; fetch('/api/auto-download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ start_date: startDate, end_date: endDate || startDate }) }) .then(res => res.json()) .then(data => { if (data.success) { statusText.textContent = data.message; // 开始轮询状态 pollAutoDownloadStatus(); } else { statusText.textContent = data.error || '启动失败'; btn.disabled = false; btn.innerHTML = ' 重试'; } }) .catch(err => { statusText.textContent = '请求失败: ' + err.message; btn.disabled = false; btn.innerHTML = ' 重试'; }); } function pollAutoDownloadStatus() { if (autoDownloadPollTimer) clearInterval(autoDownloadPollTimer); autoDownloadPollTimer = setInterval(() => { fetch('/api/auto-download/status') .then(res => res.json()) .then(data => { if (data.success) { const status = data.status; const statusText = document.getElementById('autoDownloadStatusText'); const btn = document.getElementById('autoDownloadBtn'); statusText.textContent = status.message; if (!status.running) { clearInterval(autoDownloadPollTimer); autoDownloadPollTimer = null; btn.disabled = false; btn.innerHTML = ' 开始下载'; if (status.last_file) { // 下载成功,刷新文件列表 setTimeout(() => { closeAutoDownloadModal(); loadFileList(); }, 1500); } } } }) .catch(() => { }); }, 2000); } function showLoading(show, text) { elements.loadingOverlay.style.display = show ? 'flex' : 'none'; if (text) elements.loadingText.textContent = text; } function cleanupFiles() { if (!confirm('确认清理旧文件?')) return; fetch('/cleanup', { method: 'POST' }) .then(res => res.json()) .then(data => { alert(data.message); loadFileList(); if (data.deleted_count > 0 && state.currentFile) { // Check if current file still exists? Simple approach: reload list // Logic omitted for brevity, user can re-upload } }); } function exportData() { if (!state.allData) return; const data = state.filteredData || state.allData; let csv = "Date,Product,Quantity,Price,Amount\n"; data.daily_summary.forEach(day => { day.products.forEach(p => { csv += `${day.date},${p.product},${p.quantity},${p.price},${p.amount}\n`; }); }); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'sales_data.csv'; a.click(); }