diff --git a/frontend/js/main.js b/frontend/js/main.js index 7aa9f29..bd41717 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -1,2729 +1,2996 @@ // 封装带 Token 的 fetch (全局函数) async function authenticatedFetch(url, options = {}) { - const token = localStorage.getItem('crmToken'); - const headers = { - ...options.headers, - 'Authorization': `Bearer ${token}` - }; + const token = localStorage.getItem("crmToken"); + const headers = { + ...options.headers, + Authorization: `Bearer ${token}`, + }; - const response = await fetch(url, { ...options, headers }); + const response = await fetch(url, { ...options, headers }); - if (response.status === 401) { - localStorage.removeItem('crmToken'); - window.location.href = '/static/login.html'; - return; - } + if (response.status === 401) { + localStorage.removeItem("crmToken"); + window.location.href = "/static/login.html"; + return; + } - return response; + return response; } // 解析 JWT Token 获取用户信息 function parseJwtToken(token) { - try { - if (!token) return null; - const base64Url = token.split('.')[1]; - const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }).join('')); - return JSON.parse(jsonPayload); - } catch (e) { - console.error('Error parsing JWT token:', e); - return null; - } + try { + if (!token) return null; + const base64Url = token.split(".")[1]; + const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); + const jsonPayload = decodeURIComponent( + atob(base64) + .split("") + .map(function (c) { + return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join(""), + ); + return JSON.parse(jsonPayload); + } catch (e) { + console.error("Error parsing JWT token:", e); + return null; + } } // 获取当前用户角色 function getUserRole() { - const token = localStorage.getItem('crmToken'); - const payload = parseJwtToken(token); - return payload ? payload.role : null; + const token = localStorage.getItem("crmToken"); + const payload = parseJwtToken(token); + return payload ? payload.role : null; } // 获取当前用户名 function getUsername() { - const token = localStorage.getItem('crmToken'); - const payload = parseJwtToken(token); - return payload ? payload.username : null; + const token = localStorage.getItem("crmToken"); + const payload = parseJwtToken(token); + return payload ? payload.username : null; } // 检查当前用户是否可以删除数据 // admin 用户 (role: viewer) 不能删除 // administrator 用户 (role: admin) 可以删除 function canDelete() { - const role = getUserRole(); - return role === 'admin'; + const role = getUserRole(); + return role === "admin"; } -document.addEventListener('DOMContentLoaded', function () { - // 登录守卫 - const token = localStorage.getItem('crmToken'); - if (!token && !window.location.pathname.endsWith('login.html')) { - window.location.href = '/static/login.html'; +document.addEventListener("DOMContentLoaded", function () { + // 登录守卫 + const token = localStorage.getItem("crmToken"); + if (!token && !window.location.pathname.endsWith("login.html")) { + window.location.href = "/static/login.html"; + return; + } + + // Navigation + const navItems = document.querySelectorAll(".nav-item"); + const customerSection = document.getElementById("customerSection"); + const dashboardSection = document.getElementById("dashboardSection"); + const pageTitle = document.getElementById("pageTitle"); + + // Elements + const createCustomerForm = document.getElementById("createCustomerForm"); + const importFileForm = document.getElementById("importFileForm"); + const customerTableBody = document.getElementById("customerTableBody"); + const addCustomerBtn = document.getElementById("addCustomerBtn"); + const importBtn = document.getElementById("importBtn"); + const createModal = document.getElementById("createModal"); + const importModal = document.getElementById("importModal"); + const editModal = document.getElementById("editModal"); + const editCustomerForm = document.getElementById("editCustomerForm"); + const menuToggle = document.getElementById("menuToggle"); + const sidebar = document.querySelector(".sidebar"); + const customerFilter = document.getElementById("customerFilter"); + const customerSearchInput = document.getElementById("customerSearchInput"); + const contentArea = document.querySelector(".content-area"); + const refreshCustomersBtn = document.getElementById("refreshCustomersBtn"); + const exportCustomersBtn = document.getElementById("exportCustomersBtn"); + + let allCustomers = []; + let filteredCustomers = []; + let dashboardCustomers = []; + + // Chart instances + let statusChartInstance = null; + let typeChartInstance = null; + let trendChartInstance = null; + + // Current section tracking + let currentSection = "dashboard"; + + // Pagination state + let currentPage = 1; + let pageSize = 10; + let totalPages = 1; + let totalItems = 0; + + let selectedCustomerFilter = ""; + let selectedTypeFilter = ""; + let selectedStatusProgressFilter = ""; + let customerStartDate = ""; + let customerEndDate = ""; + let customerSearchQuery = ""; + + let cellTooltipEl = null; + let tooltipAnchorCell = null; + let tooltipHideTimer = null; + + function ensureCellTooltip() { + if (cellTooltipEl) return cellTooltipEl; + + cellTooltipEl = document.createElement("div"); + cellTooltipEl.className = "cell-tooltip"; + cellTooltipEl.style.display = "none"; + document.body.appendChild(cellTooltipEl); + + cellTooltipEl.addEventListener("mouseenter", () => { + if (tooltipHideTimer) { + clearTimeout(tooltipHideTimer); + tooltipHideTimer = null; + } + }); + + cellTooltipEl.addEventListener("mouseleave", (e) => { + const nextTarget = e.relatedTarget; + if ( + tooltipAnchorCell && + nextTarget && + tooltipAnchorCell.contains(nextTarget) + ) return; + hideCellTooltip(0); + }); + + return cellTooltipEl; + } + + function hideCellTooltip(delayMs = 120) { + if (tooltipHideTimer) clearTimeout(tooltipHideTimer); + tooltipHideTimer = setTimeout(() => { + if (!cellTooltipEl) return; + cellTooltipEl.style.display = "none"; + cellTooltipEl.textContent = ""; + cellTooltipEl.removeAttribute("data-placement"); + tooltipAnchorCell = null; + }, delayMs); + } + + function positionCellTooltipForCell(cell) { + if (!cellTooltipEl) return; + + const rect = cell.getBoundingClientRect(); + const viewportPadding = 12; + const offset = 10; + + cellTooltipEl.style.left = "0px"; + cellTooltipEl.style.top = "0px"; + cellTooltipEl.style.visibility = "hidden"; + cellTooltipEl.style.display = "block"; + + const tipRect = cellTooltipEl.getBoundingClientRect(); + const tipWidth = tipRect.width; + const tipHeight = tipRect.height; + + const canShowAbove = rect.top >= tipHeight + offset + viewportPadding; + const placement = canShowAbove ? "top" : "bottom"; + cellTooltipEl.setAttribute("data-placement", placement); + + const leftUnclamped = rect.left + rect.width / 2 - tipWidth / 2; + const left = Math.max( + viewportPadding, + Math.min(leftUnclamped, window.innerWidth - viewportPadding - tipWidth), + ); + + const top = + placement === "top" + ? rect.top - tipHeight - offset + : Math.min( + rect.bottom + offset, + window.innerHeight - viewportPadding - tipHeight, + ); + + cellTooltipEl.style.left = `${Math.round(left)}px`; + cellTooltipEl.style.top = `${Math.round(top)}px`; + cellTooltipEl.style.visibility = "visible"; + } + + function showCellTooltipForCell(cell) { + const rawText = (cell.getAttribute("data-tooltip") || "").trim(); + if (!rawText) return; + + const tip = ensureCellTooltip(); + tip.textContent = rawText; + tooltipAnchorCell = cell; + + if (tooltipHideTimer) { + clearTimeout(tooltipHideTimer); + tooltipHideTimer = null; } - // Navigation - const navItems = document.querySelectorAll('.nav-item'); - const customerSection = document.getElementById('customerSection'); - const dashboardSection = document.getElementById('dashboardSection'); - const pageTitle = document.getElementById('pageTitle'); + positionCellTooltipForCell(cell); + } - // Elements - const createCustomerForm = document.getElementById('createCustomerForm'); - const importFileForm = document.getElementById('importFileForm'); - const customerTableBody = document.getElementById('customerTableBody'); - const addCustomerBtn = document.getElementById('addCustomerBtn'); - const importBtn = document.getElementById('importBtn'); - const createModal = document.getElementById('createModal'); - const importModal = document.getElementById('importModal'); - const editModal = document.getElementById('editModal'); - const editCustomerForm = document.getElementById('editCustomerForm'); - const menuToggle = document.getElementById('menuToggle'); - const sidebar = document.querySelector('.sidebar'); - const customerFilter = document.getElementById('customerFilter'); - const customerSearchInput = document.getElementById('customerSearchInput'); - const contentArea = document.querySelector('.content-area'); - const refreshCustomersBtn = document.getElementById('refreshCustomersBtn'); - const exportCustomersBtn = document.getElementById('exportCustomersBtn'); + function bindCellTooltipEvents() { + customerTableBody.addEventListener("mouseover", (e) => { + const cell = + e.target && e.target.closest + ? e.target.closest("td.has-overflow") + : null; + if (!cell || !customerTableBody.contains(cell)) return; - let allCustomers = []; - let filteredCustomers = []; - let dashboardCustomers = []; + if ( + tooltipAnchorCell === cell && + cellTooltipEl && + cellTooltipEl.style.display === "block" + ) + return; + showCellTooltipForCell(cell); + }); - // Chart instances - let statusChartInstance = null; - let typeChartInstance = null; - let trendChartInstance = null; + customerTableBody.addEventListener("mouseout", (e) => { + const fromCell = + e.target && e.target.closest + ? e.target.closest("td.has-overflow") + : null; + if (!fromCell) return; + const nextTarget = e.relatedTarget; + if (cellTooltipEl && nextTarget && cellTooltipEl.contains(nextTarget)) + return; + if (tooltipAnchorCell === fromCell) hideCellTooltip(120); + }); - // Current section tracking - let currentSection = 'dashboard'; + const reposition = () => { + if ( + !cellTooltipEl || + cellTooltipEl.style.display !== "block" || + !tooltipAnchorCell + ) + return; + positionCellTooltipForCell(tooltipAnchorCell); + }; - // Pagination state - let currentPage = 1; - let pageSize = 10; - let totalPages = 1; - let totalItems = 0; + window.addEventListener("resize", reposition); + window.addEventListener("scroll", reposition, true); + if (contentArea) + contentArea.addEventListener("scroll", reposition, { passive: true }); + } - let selectedCustomerFilter = ''; - let selectedTypeFilter = ''; - let selectedStatusProgressFilter = ''; - let customerStartDate = ''; - let customerEndDate = ''; - let customerSearchQuery = ''; + bindCellTooltipEvents(); - let cellTooltipEl = null; - let tooltipAnchorCell = null; - let tooltipHideTimer = null; + // Setup module dropdown with "other" option handling + function setupModuleDropdown(selectId, otherInputId) { + const select = document.getElementById(selectId); + const otherInput = document.getElementById(otherInputId); - function ensureCellTooltip() { - if (cellTooltipEl) return cellTooltipEl; + if (!select || !otherInput) return; - cellTooltipEl = document.createElement('div'); - cellTooltipEl.className = 'cell-tooltip'; - cellTooltipEl.style.display = 'none'; - document.body.appendChild(cellTooltipEl); + select.addEventListener("change", function () { + if (this.value === "其他") { + otherInput.style.display = "block"; + otherInput.focus(); + } else { + otherInput.style.display = "none"; + otherInput.value = ""; + } + }); + } - cellTooltipEl.addEventListener('mouseenter', () => { - if (tooltipHideTimer) { - clearTimeout(tooltipHideTimer); - tooltipHideTimer = null; - } - }); + // Get module value (handles "other" option) + function getModuleValue(selectId, otherInputId) { + const select = document.getElementById(selectId); + const otherInput = document.getElementById(otherInputId); - cellTooltipEl.addEventListener('mouseleave', (e) => { - const nextTarget = e.relatedTarget; - if (tooltipAnchorCell && nextTarget && tooltipAnchorCell.contains(nextTarget)) return; - hideCellTooltip(0); - }); + if (!select) return ""; - return cellTooltipEl; + if (select.value === "其他" && otherInput && otherInput.value.trim()) { + return otherInput.value.trim(); } + return select.value; + } - function hideCellTooltip(delayMs = 120) { - if (tooltipHideTimer) clearTimeout(tooltipHideTimer); - tooltipHideTimer = setTimeout(() => { - if (!cellTooltipEl) return; - cellTooltipEl.style.display = 'none'; - cellTooltipEl.textContent = ''; - cellTooltipEl.removeAttribute('data-placement'); - tooltipAnchorCell = null; - }, delayMs); + // Set module value in form (handles "other" option) + function setModuleValue(selectId, otherInputId, value) { + const select = document.getElementById(selectId); + const otherInput = document.getElementById(otherInputId); + + if (!select) return; + + // Check if value matches one of the predefined options + const predefinedOptions = [ + "数据生成", + "数据集", + "数据空间", + "模型工坊", + "模型广场", + "镜像管理", + "", + ]; + if (predefinedOptions.includes(value)) { + select.value = value; + if (otherInput) { + otherInput.style.display = "none"; + otherInput.value = ""; + } + } else if (value) { + // Custom value - select "other" and fill input + select.value = "其他"; + if (otherInput) { + otherInput.style.display = "block"; + otherInput.value = value; + } + } else { + select.value = ""; + if (otherInput) { + otherInput.style.display = "none"; + otherInput.value = ""; + } } + } - function positionCellTooltipForCell(cell) { - if (!cellTooltipEl) return; + // Initialize module dropdowns + setupModuleDropdown("createModule", "createModuleOther"); + setupModuleDropdown("editModule", "editModuleOther"); - const rect = cell.getBoundingClientRect(); - const viewportPadding = 12; - const offset = 10; + function normalizeDateValue(dateValue) { + const raw = String(dateValue || "").trim(); + if (!raw) return ""; - cellTooltipEl.style.left = '0px'; - cellTooltipEl.style.top = '0px'; - cellTooltipEl.style.visibility = 'hidden'; - cellTooltipEl.style.display = 'block'; + const cleaned = raw.replace(/\./g, "-").replace(/\//g, "-"); + const parts = cleaned.split("-").filter(Boolean); + if (parts.length < 3) return ""; - const tipRect = cellTooltipEl.getBoundingClientRect(); - const tipWidth = tipRect.width; - const tipHeight = tipRect.height; + const year = parts[0].padStart(4, "0"); + const month = parts[1].padStart(2, "0"); + const day = parts[2].padStart(2, "0"); + return `${year}-${month}-${day}`; + } - const canShowAbove = rect.top >= tipHeight + offset + viewportPadding; - const placement = canShowAbove ? 'top' : 'bottom'; - cellTooltipEl.setAttribute('data-placement', placement); + // Navigation event listeners + navItems.forEach((item) => { + item.addEventListener("click", function (e) { + e.preventDefault(); + const section = this.getAttribute("data-section"); + switchSection(section); + }); + }); - const leftUnclamped = rect.left + rect.width / 2 - tipWidth / 2; - const left = Math.max(viewportPadding, Math.min(leftUnclamped, window.innerWidth - viewportPadding - tipWidth)); + // Menu toggle for mobile + menuToggle.addEventListener("click", function () { + sidebar.classList.toggle("open"); + }); - const top = placement === 'top' - ? rect.top - tipHeight - offset - : Math.min(rect.bottom + offset, window.innerHeight - viewportPadding - tipHeight); - - cellTooltipEl.style.left = `${Math.round(left)}px`; - cellTooltipEl.style.top = `${Math.round(top)}px`; - cellTooltipEl.style.visibility = 'visible'; + // Close sidebar when clicking outside on mobile + document.addEventListener("click", function (e) { + if (window.innerWidth <= 768) { + if (!sidebar.contains(e.target) && !menuToggle.contains(e.target)) { + sidebar.classList.remove("open"); + } } + }); - function showCellTooltipForCell(cell) { - const rawText = (cell.getAttribute('data-tooltip') || '').trim(); - if (!rawText) return; + // Sidebar toggle functionality + const sidebarToggle = document.getElementById("sidebarToggle"); - const tip = ensureCellTooltip(); - tip.textContent = rawText; - tooltipAnchorCell = cell; + if (sidebarToggle && sidebar) { + sidebarToggle.addEventListener("click", function () { + sidebar.classList.toggle("collapsed"); - if (tooltipHideTimer) { - clearTimeout(tooltipHideTimer); - tooltipHideTimer = null; - } + // Update button title + if (sidebar.classList.contains("collapsed")) { + sidebarToggle.title = "展开侧边栏"; + } else { + sidebarToggle.title = "收起侧边栏"; + } + }); + } - positionCellTooltipForCell(cell); + // Add Customer button + addCustomerBtn.addEventListener("click", async function () { + await loadTrialCustomerListForCreate(); + createModal.style.display = "block"; + }); + + // Import button + importBtn.addEventListener("click", function () { + importModal.style.display = "block"; + }); + + // Close create modal + createModal.querySelector(".close").addEventListener("click", function () { + createModal.style.display = "none"; + }); + + createModal + .querySelector(".cancel-create") + .addEventListener("click", function () { + createModal.style.display = "none"; + }); + + // Close import modal + importModal.querySelector(".close").addEventListener("click", function () { + importModal.style.display = "none"; + }); + + importModal + .querySelector(".cancel-import") + .addEventListener("click", function () { + importModal.style.display = "none"; + }); + + // Close edit modal + editModal.querySelector(".close").addEventListener("click", function () { + editModal.style.display = "none"; + }); + + editModal + .querySelector(".cancel-edit") + .addEventListener("click", function () { + editModal.style.display = "none"; + }); + + // File name display + const importFile = document.getElementById("importFile"); + const fileName = document.getElementById("fileName"); + + importFile.addEventListener("change", function () { + if (this.files.length > 0) { + fileName.textContent = this.files[0].name; + } else { + fileName.textContent = "选择文件..."; } + }); - function bindCellTooltipEvents() { - customerTableBody.addEventListener('mouseover', (e) => { - const cell = e.target && e.target.closest ? e.target.closest('td.has-overflow') : null; - if (!cell || !customerTableBody.contains(cell)) return; + // Customer filter change event + customerFilter.addEventListener("change", function () { + selectedCustomerFilter = this.value; + currentPage = 1; + applyAllCustomerFilters(); + }); - if (tooltipAnchorCell === cell && cellTooltipEl && cellTooltipEl.style.display === 'block') return; - showCellTooltipForCell(cell); - }); - - customerTableBody.addEventListener('mouseout', (e) => { - const fromCell = e.target && e.target.closest ? e.target.closest('td.has-overflow') : null; - if (!fromCell) return; - const nextTarget = e.relatedTarget; - if (cellTooltipEl && nextTarget && cellTooltipEl.contains(nextTarget)) return; - if (tooltipAnchorCell === fromCell) hideCellTooltip(120); - }); - - const reposition = () => { - if (!cellTooltipEl || cellTooltipEl.style.display !== 'block' || !tooltipAnchorCell) return; - positionCellTooltipForCell(tooltipAnchorCell); - }; - - window.addEventListener('resize', reposition); - window.addEventListener('scroll', reposition, true); - if (contentArea) contentArea.addEventListener('scroll', reposition, { passive: true }); - } - - bindCellTooltipEvents(); - - // Setup module dropdown with "other" option handling - function setupModuleDropdown(selectId, otherInputId) { - const select = document.getElementById(selectId); - const otherInput = document.getElementById(otherInputId); - - if (!select || !otherInput) return; - - select.addEventListener('change', function () { - if (this.value === '其他') { - otherInput.style.display = 'block'; - otherInput.focus(); - } else { - otherInput.style.display = 'none'; - otherInput.value = ''; - } - }); - } - - // Get module value (handles "other" option) - function getModuleValue(selectId, otherInputId) { - const select = document.getElementById(selectId); - const otherInput = document.getElementById(otherInputId); - - if (!select) return ''; - - if (select.value === '其他' && otherInput && otherInput.value.trim()) { - return otherInput.value.trim(); - } - return select.value; - } - - // Set module value in form (handles "other" option) - function setModuleValue(selectId, otherInputId, value) { - const select = document.getElementById(selectId); - const otherInput = document.getElementById(otherInputId); - - if (!select) return; - - // Check if value matches one of the predefined options - const predefinedOptions = ['数据生成', '数据集', '数据空间', '模型工坊', '模型广场', '镜像管理', '']; - if (predefinedOptions.includes(value)) { - select.value = value; - if (otherInput) { - otherInput.style.display = 'none'; - otherInput.value = ''; - } - } else if (value) { - // Custom value - select "other" and fill input - select.value = '其他'; - if (otherInput) { - otherInput.style.display = 'block'; - otherInput.value = value; - } - } else { - select.value = ''; - if (otherInput) { - otherInput.style.display = 'none'; - otherInput.value = ''; - } - } - } - - // Initialize module dropdowns - setupModuleDropdown('createModule', 'createModuleOther'); - setupModuleDropdown('editModule', 'editModuleOther'); - - function normalizeDateValue(dateValue) { - const raw = String(dateValue || '').trim(); - if (!raw) return ''; - - const cleaned = raw.replace(/\./g, '-').replace(/\//g, '-'); - const parts = cleaned.split('-').filter(Boolean); - if (parts.length < 3) return ''; - - const year = parts[0].padStart(4, '0'); - const month = parts[1].padStart(2, '0'); - const day = parts[2].padStart(2, '0'); - return `${year}-${month}-${day}`; - } - - // Navigation event listeners - navItems.forEach(item => { - item.addEventListener('click', function (e) { - e.preventDefault(); - const section = this.getAttribute('data-section'); - switchSection(section); - }); - }); - - // Menu toggle for mobile - menuToggle.addEventListener('click', function () { - sidebar.classList.toggle('open'); - }); - - // Close sidebar when clicking outside on mobile - document.addEventListener('click', function (e) { - if (window.innerWidth <= 768) { - if (!sidebar.contains(e.target) && !menuToggle.contains(e.target)) { - sidebar.classList.remove('open'); - } - } - }); - - // Sidebar toggle functionality - const sidebarToggle = document.getElementById('sidebarToggle'); - - if (sidebarToggle && sidebar) { - sidebarToggle.addEventListener('click', function () { - sidebar.classList.toggle('collapsed'); - - // Update button title - if (sidebar.classList.contains('collapsed')) { - sidebarToggle.title = '展开侧边栏'; - } else { - sidebarToggle.title = '收起侧边栏'; - } - }); - } - - // Add Customer button - addCustomerBtn.addEventListener('click', async function () { - await loadTrialCustomerListForCreate(); - createModal.style.display = 'block'; - }); - - // Import button - importBtn.addEventListener('click', function () { - importModal.style.display = 'block'; - }); - - // Close create modal - createModal.querySelector('.close').addEventListener('click', function () { - createModal.style.display = 'none'; - }); - - createModal.querySelector('.cancel-create').addEventListener('click', function () { - createModal.style.display = 'none'; - }); - - // Close import modal - importModal.querySelector('.close').addEventListener('click', function () { - importModal.style.display = 'none'; - }); - - importModal.querySelector('.cancel-import').addEventListener('click', function () { - importModal.style.display = 'none'; - }); - - // Close edit modal - editModal.querySelector('.close').addEventListener('click', function () { - editModal.style.display = 'none'; - }); - - editModal.querySelector('.cancel-edit').addEventListener('click', function () { - editModal.style.display = 'none'; - }); - - // File name display - const importFile = document.getElementById('importFile'); - const fileName = document.getElementById('fileName'); - - importFile.addEventListener('change', function () { - if (this.files.length > 0) { - fileName.textContent = this.files[0].name; - } else { - fileName.textContent = '选择文件...'; - } - }); - - // Customer filter change event - customerFilter.addEventListener('change', function () { - selectedCustomerFilter = this.value; + if (customerSearchInput) { + customerSearchInput.addEventListener("input", function () { + customerSearchQuery = (this.value || "").trim(); + if (currentSection === "customer") { currentPage = 1; applyAllCustomerFilters(); + } + }); + } + + // Load trial customer list for create customer dropdown + async function loadTrialCustomerListForCreate() { + try { + const response = await authenticatedFetch("/api/trial-customers/list"); + const data = await response.json(); + + const customerSelect = document.getElementById("createCustomerName"); + customerSelect.innerHTML = '请选择客户'; + + if (data.customerNames && data.customerNames.length > 0) { + data.customerNames.forEach((customerName) => { + const option = document.createElement("option"); + option.value = customerName; + option.textContent = customerName; + customerSelect.appendChild(option); + }); + } + } catch (error) { + console.error("Error loading trial customer list:", error); + } + } + + // Type filter change event + const typeFilter = document.getElementById("typeFilter"); + if (typeFilter) { + typeFilter.addEventListener("change", function () { + selectedTypeFilter = this.value; + currentPage = 1; + applyAllCustomerFilters(); + }); + } + + // Customer date range filter events + const customerStartDateInput = document.getElementById("customerStartDate"); + const customerEndDateInput = document.getElementById("customerEndDate"); + + if (customerStartDateInput) { + customerStartDateInput.addEventListener("change", function () { + customerStartDate = this.value; + currentPage = 1; + applyAllCustomerFilters(); + }); + } + + if (customerEndDateInput) { + customerEndDateInput.addEventListener("change", function () { + customerEndDate = this.value; + currentPage = 1; + applyAllCustomerFilters(); + }); + } + + // Status progress filter change event + const statusProgressFilter = document.getElementById("statusProgressFilter"); + if (statusProgressFilter) { + statusProgressFilter.addEventListener("change", function () { + selectedStatusProgressFilter = this.value; + currentPage = 1; + applyAllCustomerFilters(); + }); + } + + // Apply date filter for dashboard + document + .getElementById("applyFilters") + .addEventListener("click", function () { + const startDate = document.getElementById("startDate").value; + const endDate = document.getElementById("endDate").value; + applyDateFilter(startDate, endDate); }); - if (customerSearchInput) { - customerSearchInput.addEventListener('input', function () { - customerSearchQuery = (this.value || '').trim(); - if (currentSection === 'customer') { - currentPage = 1; - applyAllCustomerFilters(); - } - }); - } + // Chart title change events - 添加空值检查防止元素不存在时报错 + const statusChartTitleEl = document.getElementById("statusChartTitle"); + if (statusChartTitleEl) { + statusChartTitleEl.addEventListener("input", function () { + applyDateFilter( + document.getElementById("startDate").value, + document.getElementById("endDate").value, + ); + }); + } - // Load trial customer list for create customer dropdown - async function loadTrialCustomerListForCreate() { - try { - const response = await authenticatedFetch('/api/trial-customers/list'); - const data = await response.json(); + const typeChartTitleEl = document.getElementById("typeChartTitle"); + if (typeChartTitleEl) { + typeChartTitleEl.addEventListener("input", function () { + applyDateFilter( + document.getElementById("startDate").value, + document.getElementById("endDate").value, + ); + }); + } - const customerSelect = document.getElementById('createCustomerName'); - customerSelect.innerHTML = '请选择客户'; - - if (data.customerNames && data.customerNames.length > 0) { - data.customerNames.forEach(customerName => { - const option = document.createElement('option'); - option.value = customerName; - option.textContent = customerName; - customerSelect.appendChild(option); - }); - } - } catch (error) { - console.error('Error loading trial customer list:', error); - } - } - - - // Type filter change event - const typeFilter = document.getElementById('typeFilter'); - if (typeFilter) { - typeFilter.addEventListener('change', function () { - selectedTypeFilter = this.value; - currentPage = 1; - applyAllCustomerFilters(); - }); - } - - // Customer date range filter events - const customerStartDateInput = document.getElementById('customerStartDate'); - const customerEndDateInput = document.getElementById('customerEndDate'); - - if (customerStartDateInput) { - customerStartDateInput.addEventListener('change', function () { - customerStartDate = this.value; - currentPage = 1; - applyAllCustomerFilters(); - }); - } - - if (customerEndDateInput) { - customerEndDateInput.addEventListener('change', function () { - customerEndDate = this.value; - currentPage = 1; - applyAllCustomerFilters(); - }); - } - - // Status progress filter change event - const statusProgressFilter = document.getElementById('statusProgressFilter'); - if (statusProgressFilter) { - statusProgressFilter.addEventListener('change', function () { - selectedStatusProgressFilter = this.value; - currentPage = 1; - applyAllCustomerFilters(); - }); - } - - // Apply date filter for dashboard - document.getElementById('applyFilters').addEventListener('click', function () { - const startDate = document.getElementById('startDate').value; - const endDate = document.getElementById('endDate').value; - applyDateFilter(startDate, endDate); + // Switch between sections + function switchSection(section) { + navItems.forEach((item) => { + item.classList.remove("active"); }); - // Chart title change events - document.getElementById('statusChartTitle').addEventListener('input', function () { - applyDateFilter(document.getElementById('startDate').value, document.getElementById('endDate').value); - }); + // Hide all sections first + customerSection.classList.remove("active"); + dashboardSection.classList.remove("active"); + if (document.getElementById("followupSection")) { + document.getElementById("followupSection").classList.remove("active"); + } + if (document.getElementById("trialPeriodsSection")) { + document.getElementById("trialPeriodsSection").classList.remove("active"); + } - document.getElementById('typeChartTitle').addEventListener('input', function () { - applyDateFilter(document.getElementById('startDate').value, document.getElementById('endDate').value); - }); + if (section === "customer") { + customerSection.classList.add("active"); + document + .querySelector('[data-section="customer"]') + .classList.add("active"); + pageTitle.textContent = "每周客户进度"; + currentSection = "customer"; + loadCustomers(); + } else if (section === "trialPeriods") { + const trialPeriodsSection = document.getElementById( + "trialPeriodsSection", + ); + if (trialPeriodsSection) { + trialPeriodsSection.classList.add("active"); + } + document + .querySelector('[data-section="trialPeriods"]') + .classList.add("active"); + pageTitle.textContent = "客户信息"; + currentSection = "trialPeriods"; - // Switch between sections - function switchSection(section) { - navItems.forEach(item => { - item.classList.remove('active'); + // Load customers first, then trial periods + if ( + typeof loadCustomersForDropdown === "function" && + typeof loadAllTrialPeriods === "function" + ) { + loadCustomersForDropdown().then(() => { + loadAllTrialPeriods(); }); - - // Hide all sections first - customerSection.classList.remove('active'); - dashboardSection.classList.remove('active'); - if (document.getElementById('followupSection')) { - document.getElementById('followupSection').classList.remove('active'); - } - if (document.getElementById('trialPeriodsSection')) { - document.getElementById('trialPeriodsSection').classList.remove('active'); - } - - if (section === 'customer') { - customerSection.classList.add('active'); - document.querySelector('[data-section="customer"]').classList.add('active'); - pageTitle.textContent = '每周客户进度'; - currentSection = 'customer'; - loadCustomers(); - } else if (section === 'trialPeriods') { - const trialPeriodsSection = document.getElementById('trialPeriodsSection'); - if (trialPeriodsSection) { - trialPeriodsSection.classList.add('active'); - } - document.querySelector('[data-section="trialPeriods"]').classList.add('active'); - pageTitle.textContent = '客户信息'; - currentSection = 'trialPeriods'; - - // Load customers first, then trial periods - if (typeof loadCustomersForDropdown === 'function' && typeof loadAllTrialPeriods === 'function') { - loadCustomersForDropdown().then(() => { - loadAllTrialPeriods(); - }); - } else { - console.error('Required functions not available!'); - } - } else if (section === 'dashboard') { - dashboardSection.classList.add('active'); - document.querySelector('[data-section="dashboard"]').classList.add('active'); - pageTitle.textContent = '数据仪表板'; - currentSection = 'dashboard'; - loadDashboardData(); - } else if (section === 'followup') { - const followupSection = document.getElementById('followupSection'); - if (followupSection) { - followupSection.classList.add('active'); - } - document.querySelector('[data-section="followup"]').classList.add('active'); - pageTitle.textContent = '客户跟进'; - currentSection = 'followup'; - if (typeof loadFollowUps === 'function') { - loadFollowUps(); - } - } - - // Close sidebar on mobile after navigation - if (window.innerWidth <= 768) { - sidebar.classList.remove('open'); - } + } else { + console.error("Required functions not available!"); + } + } else if (section === "dashboard") { + dashboardSection.classList.add("active"); + document + .querySelector('[data-section="dashboard"]') + .classList.add("active"); + pageTitle.textContent = "数据仪表板"; + currentSection = "dashboard"; + loadDashboardData(); + } else if (section === "followup") { + const followupSection = document.getElementById("followupSection"); + if (followupSection) { + followupSection.classList.add("active"); + } + document + .querySelector('[data-section="followup"]') + .classList.add("active"); + pageTitle.textContent = "客户跟进"; + currentSection = "followup"; + if (typeof loadFollowUps === "function") { + loadFollowUps(); + } } - // Load customers from API - async function loadCustomers() { - try { - const response = await authenticatedFetch('/api/customers?page=1&pageSize=1000'); - const data = await response.json(); - - if (data.customers) { - allCustomers = data.customers; - populateCustomerFilter(); - populateTypeFilter(); - applyAllCustomerFilters(); - } - } catch (error) { - console.error('Error loading customers:', error); - } + // Close sidebar on mobile after navigation + if (window.innerWidth <= 768) { + sidebar.classList.remove("open"); } + } - // Populate customer filter dropdown - function populateCustomerFilter() { - const uniqueCustomers = [...new Set(allCustomers.map(c => c.customerName).filter(c => c))]; - customerFilter.innerHTML = '全部客户'; + // Load customers from API + async function loadCustomers() { + try { + const response = await authenticatedFetch( + "/api/customers?page=1&pageSize=1000", + ); + const data = await response.json(); - uniqueCustomers.forEach(customer => { - const option = document.createElement('option'); - option.value = customer; - option.textContent = customer; - customerFilter.appendChild(option); - }); - - if (selectedCustomerFilter) { - customerFilter.value = selectedCustomerFilter; - } - } - - // Populate type filter dropdown - function populateTypeFilter() { - const typeFilterElement = document.getElementById('typeFilter'); - if (!typeFilterElement) return; - - const uniqueTypes = [...new Set(allCustomers.map(c => c.type).filter(t => t))]; - typeFilterElement.innerHTML = '全部类型'; - - uniqueTypes.forEach(type => { - const option = document.createElement('option'); - option.value = type; - option.textContent = type; - typeFilterElement.appendChild(option); - }); - - if (selectedTypeFilter) { - typeFilterElement.value = selectedTypeFilter; - } - } - - // Filter customers by selected customer - function filterCustomers(selectedCustomer) { - selectedCustomerFilter = selectedCustomer; - currentPage = 1; + if (data.customers) { + allCustomers = data.customers; + populateCustomerFilter(); + populateTypeFilter(); applyAllCustomerFilters(); + } + } catch (error) { + console.error("Error loading customers:", error); + } + } + + // Populate customer filter dropdown + function populateCustomerFilter() { + const uniqueCustomers = [ + ...new Set(allCustomers.map((c) => c.customerName).filter((c) => c)), + ]; + customerFilter.innerHTML = '全部客户'; + + uniqueCustomers.forEach((customer) => { + const option = document.createElement("option"); + option.value = customer; + option.textContent = customer; + customerFilter.appendChild(option); + }); + + if (selectedCustomerFilter) { + customerFilter.value = selectedCustomerFilter; + } + } + + // Populate type filter dropdown + function populateTypeFilter() { + const typeFilterElement = document.getElementById("typeFilter"); + if (!typeFilterElement) return; + + const uniqueTypes = [ + ...new Set(allCustomers.map((c) => c.type).filter((t) => t)), + ]; + typeFilterElement.innerHTML = '全部类型'; + + uniqueTypes.forEach((type) => { + const option = document.createElement("option"); + option.value = type; + option.textContent = type; + typeFilterElement.appendChild(option); + }); + + if (selectedTypeFilter) { + typeFilterElement.value = selectedTypeFilter; + } + } + + // Filter customers by selected customer + function filterCustomers(selectedCustomer) { + selectedCustomerFilter = selectedCustomer; + currentPage = 1; + applyAllCustomerFilters(); + } + + function customerMatchesQuery(customer, query) { + if (!query) return true; + + const fields = [ + customer.customerName, + customer.intendedProduct, + customer.version, + customer.description, + customer.solution, + customer.type, + customer.module, + customer.statusProgress, + customer.reporter, + ]; + + const haystack = fields + .map((v) => String(v || "")) + .join(" ") + .toLowerCase(); + + const normalizedHaystack = haystack.replace(/[\/.]/g, "-"); + const needle = query.toLowerCase(); + const normalizedNeedle = needle.replace(/[\/.]/g, "-"); + + return normalizedHaystack.includes(normalizedNeedle); + } + + function applyAllCustomerFilters() { + let next = [...allCustomers]; + + if (selectedCustomerFilter) { + next = next.filter((c) => c.customerName === selectedCustomerFilter); } - function customerMatchesQuery(customer, query) { - if (!query) return true; - - const fields = [ - customer.customerName, - customer.intendedProduct, - customer.version, - customer.description, - customer.solution, - customer.type, - customer.module, - customer.statusProgress, - customer.reporter - ]; - - const haystack = fields - .map(v => String(v || '')) - .join(' ') - .toLowerCase(); - - const normalizedHaystack = haystack.replace(/[\/.]/g, '-'); - const needle = query.toLowerCase(); - const normalizedNeedle = needle.replace(/[\/.]/g, '-'); - - return normalizedHaystack.includes(normalizedNeedle); + if (selectedTypeFilter) { + next = next.filter((c) => c.type === selectedTypeFilter); } - function applyAllCustomerFilters() { - let next = [...allCustomers]; + if (customerStartDate) { + next = next.filter((c) => { + const date = normalizeDateValue(c.intendedProduct); + return date && date >= customerStartDate; + }); + } - if (selectedCustomerFilter) { - next = next.filter(c => c.customerName === selectedCustomerFilter); + if (customerEndDate) { + next = next.filter((c) => { + const date = normalizeDateValue(c.intendedProduct); + return date && date <= customerEndDate; + }); + } + + if (selectedStatusProgressFilter) { + next = next.filter( + (c) => c.statusProgress === selectedStatusProgressFilter, + ); + } + + if (customerSearchQuery) { + next = next.filter((c) => customerMatchesQuery(c, customerSearchQuery)); + } + + filteredCustomers = next; + applyCustomerFilter(); + } + + function toCsvCell(value) { + const raw = String(value ?? ""); + const normalized = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + const escaped = normalized.replace(/"/g, '""'); + return `"${escaped}"`; + } + + function downloadCsv(filename, rows) { + const content = ["\uFEFF" + rows[0], ...rows.slice(1)].join("\r\n"); + const blob = new Blob([content], { type: "text/csv;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } + + function exportCustomersToCsv(customers) { + const header = [ + "客户", + "咨询时间", + "版本", + "描述", + "解决方案", + "类型", + "模块", + "状态与进度", + ] + .map(toCsvCell) + .join(","); + + const lines = customers.map((c) => { + const date = + normalizeDateValue(c.intendedProduct) || c.intendedProduct || ""; + const cells = [ + c.customerName || "", + date, + c.version || "", + c.description || "", + c.solution || "", + c.type || "", + c.module || "", + c.statusProgress || "", + ]; + return cells.map(toCsvCell).join(","); + }); + + const now = new Date(); + const y = now.getFullYear(); + const m = String(now.getMonth() + 1).padStart(2, "0"); + const d = String(now.getDate()).padStart(2, "0"); + const hh = String(now.getHours()).padStart(2, "0"); + const mm = String(now.getMinutes()).padStart(2, "0"); + const ss = String(now.getSeconds()).padStart(2, "0"); + const filename = `客户列表_${y}${m}${d}_${hh}${mm}${ss}.csv`; + + downloadCsv(filename, [header, ...lines]); + } + + if (refreshCustomersBtn) { + refreshCustomersBtn.addEventListener("click", async () => { + // Add refreshing animation + refreshCustomersBtn.classList.add("refreshing"); + const table = document.getElementById("customerTable"); + try { + await loadCustomers(); + // Show success feedback briefly + refreshCustomersBtn.classList.remove("refreshing"); + refreshCustomersBtn.classList.add("refresh-success"); + // Add table refresh animation + if (table) { + table.classList.add("table-refreshing"); + setTimeout(() => { + table.classList.remove("table-refreshing"); + }, 500); + } + setTimeout(() => { + refreshCustomersBtn.classList.remove("refresh-success"); + }, 1000); + } catch (error) { + refreshCustomersBtn.classList.remove("refreshing"); + } + }); + } + + if (exportCustomersBtn) { + exportCustomersBtn.addEventListener("click", () => { + if (currentSection !== "customer") return; + exportCustomersToCsv(filteredCustomers); + }); + } + + // Apply customer filter and render table with pagination + function applyCustomerFilter() { + totalItems = filteredCustomers.length; + totalPages = Math.ceil(totalItems / pageSize); + + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedCustomers = filteredCustomers.slice(startIndex, endIndex); + + renderCustomerTable(paginatedCustomers); + updatePaginationControls(); + } + + // Load all customers for dashboard + async function loadAllCustomers() { + try { + const response = await authenticatedFetch( + "/api/customers?page=1&pageSize=1000", + ); + const data = await response.json(); + + if (data.customers) { + dashboardCustomers = data.customers; + updateDashboardStats(dashboardCustomers); + renderStatusChart(dashboardCustomers); + renderTypeChart(dashboardCustomers); + renderTrendChart(dashboardCustomers); + } else { + console.error("No customers in dashboard data"); + } + } catch (error) { + console.error("Error loading all customers:", error); + } + } + + // Apply date filter for dashboard + function applyDateFilter(startDate, endDate) { + let filteredData = dashboardCustomers; + + if (startDate) { + filteredData = filteredData.filter((c) => { + if (!c.intendedProduct) return false; + const date = normalizeDateValue(c.intendedProduct); + return date && date >= startDate; + }); + } + + if (endDate) { + filteredData = filteredData.filter((c) => { + if (!c.intendedProduct) return false; + const date = normalizeDateValue(c.intendedProduct); + return date && date <= endDate; + }); + } + + updateDashboardStats(filteredData); + renderStatusChart(filteredData); + renderTypeChart(filteredData); + renderTrendChart(filteredData); + } + + // Render customer table + function renderCustomerTable(customers) { + customerTableBody.innerHTML = ""; + + // Helper function to generate status badge + function getStatusBadge(status) { + if (!status) return ""; + const statusLower = status.toLowerCase(); + if ( + status === "已完成" || + statusLower.includes("完成") || + statusLower.includes("complete") + ) { + return ` ${status}`; + } else if (status === "进行中" || statusLower.includes("进行")) { + return ` ${status}`; + } else if (status === "待排期" || statusLower.includes("待")) { + return ` ${status}`; + } else if (status === "已驳回" || statusLower.includes("驳回")) { + return ` ${status}`; + } else if (status === "已上线" || statusLower.includes("上线")) { + return ` ${status}`; + } + return `${status}`; + } + + customers.forEach((customer) => { + const row = document.createElement("tr"); + row.classList.add("customer-row"); + + const date = customer.intendedProduct || ""; + + const fields = [ + { value: customer.customerName || "", name: "customerName" }, + { value: date, name: "date" }, + { value: customer.version || "", name: "version" }, + { value: customer.description || "", name: "description" }, + { value: customer.solution || "", name: "solution" }, + { value: customer.type || "", name: "type" }, + { value: customer.module || "", name: "module" }, + { value: customer.statusProgress || "", name: "statusProgress" }, + ]; + + fields.forEach((field) => { + const td = document.createElement("td"); + const textValue = String(field.value ?? ""); + + // Use badge for statusProgress field + if (field.name === "statusProgress") { + td.innerHTML = getStatusBadge(textValue); + } else if (field.name === "customerName") { + td.innerHTML = `${textValue}`; + } else { + td.textContent = textValue; } - if (selectedTypeFilter) { - next = next.filter(c => c.type === selectedTypeFilter); + td.setAttribute("data-tooltip", textValue); + + if (field.name === "description" || field.name === "solution") { + td.classList.add("overflow-cell"); } - if (customerStartDate) { - next = next.filter(c => { - const date = normalizeDateValue(c.intendedProduct); - return date && date >= customerStartDate; - }); - } + row.appendChild(td); + }); - if (customerEndDate) { - next = next.filter(c => { - const date = normalizeDateValue(c.intendedProduct); - return date && date <= customerEndDate; - }); - } + const actionTd = document.createElement("td"); + actionTd.classList.add("action-cell"); - if (selectedStatusProgressFilter) { - next = next.filter(c => c.statusProgress === selectedStatusProgressFilter); - } + // 根据用户权限决定删除按钮是否可用 + const deleteDisabled = !canDelete(); + const deleteClass = deleteDisabled + ? "action-btn delete-btn disabled" + : "action-btn delete-btn"; + const deleteTitle = deleteDisabled ? "无删除权限" : "删除"; - if (customerSearchQuery) { - next = next.filter(c => customerMatchesQuery(c, customerSearchQuery)); - } - - filteredCustomers = next; - applyCustomerFilter(); - } - - function toCsvCell(value) { - const raw = String(value ?? ''); - const normalized = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - const escaped = normalized.replace(/"/g, '""'); - return `"${escaped}"`; - } - - function downloadCsv(filename, rows) { - const content = ['\uFEFF' + rows[0], ...rows.slice(1)].join('\r\n'); - const blob = new Blob([content], { type: 'text/csv;charset=utf-8' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - a.remove(); - URL.revokeObjectURL(url); - } - - function exportCustomersToCsv(customers) { - const header = [ - '客户', - '咨询时间', - '版本', - '描述', - '解决方案', - '类型', - '模块', - '状态与进度' - ].map(toCsvCell).join(','); - - const lines = customers.map(c => { - const date = normalizeDateValue(c.intendedProduct) || (c.intendedProduct || ''); - const cells = [ - c.customerName || '', - date, - c.version || '', - c.description || '', - c.solution || '', - c.type || '', - c.module || '', - c.statusProgress || '' - ]; - return cells.map(toCsvCell).join(','); - }); - - const now = new Date(); - const y = now.getFullYear(); - const m = String(now.getMonth() + 1).padStart(2, '0'); - const d = String(now.getDate()).padStart(2, '0'); - const hh = String(now.getHours()).padStart(2, '0'); - const mm = String(now.getMinutes()).padStart(2, '0'); - const ss = String(now.getSeconds()).padStart(2, '0'); - const filename = `客户列表_${y}${m}${d}_${hh}${mm}${ss}.csv`; - - downloadCsv(filename, [header, ...lines]); - } - - if (refreshCustomersBtn) { - refreshCustomersBtn.addEventListener('click', async () => { - // Add refreshing animation - refreshCustomersBtn.classList.add('refreshing'); - const table = document.getElementById('customerTable'); - try { - await loadCustomers(); - // Show success feedback briefly - refreshCustomersBtn.classList.remove('refreshing'); - refreshCustomersBtn.classList.add('refresh-success'); - // Add table refresh animation - if (table) { - table.classList.add('table-refreshing'); - setTimeout(() => { - table.classList.remove('table-refreshing'); - }, 500); - } - setTimeout(() => { - refreshCustomersBtn.classList.remove('refresh-success'); - }, 1000); - } catch (error) { - refreshCustomersBtn.classList.remove('refreshing'); - } - }); - } - - if (exportCustomersBtn) { - exportCustomersBtn.addEventListener('click', () => { - if (currentSection !== 'customer') return; - exportCustomersToCsv(filteredCustomers); - }); - } - - // Apply customer filter and render table with pagination - function applyCustomerFilter() { - totalItems = filteredCustomers.length; - totalPages = Math.ceil(totalItems / pageSize); - - const startIndex = (currentPage - 1) * pageSize; - const endIndex = startIndex + pageSize; - const paginatedCustomers = filteredCustomers.slice(startIndex, endIndex); - - renderCustomerTable(paginatedCustomers); - updatePaginationControls(); - } - - // Load all customers for dashboard - async function loadAllCustomers() { - try { - const response = await authenticatedFetch('/api/customers?page=1&pageSize=1000'); - const data = await response.json(); - - if (data.customers) { - dashboardCustomers = data.customers; - updateDashboardStats(dashboardCustomers); - renderStatusChart(dashboardCustomers); - renderTypeChart(dashboardCustomers); - renderTrendChart(dashboardCustomers); - } else { - console.error('No customers in dashboard data'); - } - } catch (error) { - console.error('Error loading all customers:', error); - } - } - - // Apply date filter for dashboard - function applyDateFilter(startDate, endDate) { - let filteredData = dashboardCustomers; - - if (startDate) { - filteredData = filteredData.filter(c => { - if (!c.intendedProduct) return false; - const date = normalizeDateValue(c.intendedProduct); - return date && date >= startDate; - }); - } - - if (endDate) { - filteredData = filteredData.filter(c => { - if (!c.intendedProduct) return false; - const date = normalizeDateValue(c.intendedProduct); - return date && date <= endDate; - }); - } - - updateDashboardStats(filteredData); - renderStatusChart(filteredData); - renderTypeChart(filteredData); - renderTrendChart(filteredData); - } - - // Render customer table - function renderCustomerTable(customers) { - customerTableBody.innerHTML = ''; - - // Helper function to generate status badge - function getStatusBadge(status) { - if (!status) return ''; - const statusLower = status.toLowerCase(); - if (status === '已完成' || statusLower.includes('完成') || statusLower.includes('complete')) { - return ` ${status}`; - } else if (status === '进行中' || statusLower.includes('进行')) { - return ` ${status}`; - } else if (status === '待排期' || statusLower.includes('待')) { - return ` ${status}`; - } else if (status === '已驳回' || statusLower.includes('驳回')) { - return ` ${status}`; - } else if (status === '已上线' || statusLower.includes('上线')) { - return ` ${status}`; - } - return `${status}`; - } - - customers.forEach(customer => { - const row = document.createElement('tr'); - row.classList.add('customer-row'); - - const date = customer.intendedProduct || ''; - - - const fields = [ - { value: customer.customerName || '', name: 'customerName' }, - { value: date, name: 'date' }, - { value: customer.version || '', name: 'version' }, - { value: customer.description || '', name: 'description' }, - { value: customer.solution || '', name: 'solution' }, - { value: customer.type || '', name: 'type' }, - { value: customer.module || '', name: 'module' }, - { value: customer.statusProgress || '', name: 'statusProgress' } - ]; - - fields.forEach(field => { - const td = document.createElement('td'); - const textValue = String(field.value ?? ''); - - // Use badge for statusProgress field - if (field.name === 'statusProgress') { - td.innerHTML = getStatusBadge(textValue); - } else if (field.name === 'customerName') { - td.innerHTML = `${textValue}`; - } else { - td.textContent = textValue; - } - - td.setAttribute('data-tooltip', textValue); - - if (field.name === 'description' || field.name === 'solution') { - td.classList.add('overflow-cell'); - } - - row.appendChild(td); - }); - - const actionTd = document.createElement('td'); - actionTd.classList.add('action-cell'); - - // 根据用户权限决定删除按钮是否可用 - const deleteDisabled = !canDelete(); - const deleteClass = deleteDisabled ? 'action-btn delete-btn disabled' : 'action-btn delete-btn'; - const deleteTitle = deleteDisabled ? '无删除权限' : '删除'; - - actionTd.innerHTML = ` + actionTd.innerHTML = ` - + `; - row.appendChild(actionTd); + row.appendChild(actionTd); - customerTableBody.appendChild(row); - }); - - checkTextOverflow(); - - document.querySelectorAll('.edit-btn').forEach(btn => { - btn.addEventListener('click', function () { - const customerId = this.getAttribute('data-id'); - openEditModal(customerId); - }); - }); - - document.querySelectorAll('.delete-btn:not(.disabled)').forEach(btn => { - btn.addEventListener('click', function () { - const customerId = this.getAttribute('data-id'); - deleteCustomer(customerId); - }); - }); - } - - function checkTextOverflow() { - // Use requestAnimationFrame to ensure DOM is fully rendered before checking overflow - requestAnimationFrame(() => { - const cells = customerTableBody.querySelectorAll('td[data-tooltip]'); - cells.forEach(cell => { - const cellText = (cell.getAttribute('data-tooltip') || '').trim(); - const shouldAlwaysShowTooltip = cell.classList.contains('overflow-cell') && cellText.length > 0; - const hasOverflow = cell.scrollWidth > cell.clientWidth; - - if (shouldAlwaysShowTooltip || hasOverflow) { - cell.classList.add('has-overflow'); - } else { - cell.classList.remove('has-overflow'); - } - }); - }); - } - - // Update pagination controls - function updatePaginationControls() { - const startItem = totalItems === 0 ? 0 : (currentPage - 1) * pageSize + 1; - const endItem = Math.min(currentPage * pageSize, totalItems); - - document.getElementById('paginationInfo').textContent = - `显示 ${startItem}-${endItem} 共 ${totalItems} 条`; - - document.getElementById('firstPage').disabled = currentPage === 1; - document.getElementById('prevPage').disabled = currentPage === 1; - document.getElementById('nextPage').disabled = currentPage === totalPages; - document.getElementById('lastPage').disabled = currentPage === totalPages; - - const pageNumbers = document.getElementById('pageNumbers'); - pageNumbers.innerHTML = ''; - - const maxVisiblePages = 5; - let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2)); - let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); - - if (endPage - startPage + 1 < maxVisiblePages) { - startPage = Math.max(1, endPage - maxVisiblePages + 1); - } - - for (let i = startPage; i <= endPage; i++) { - const pageBtn = document.createElement('button'); - pageBtn.className = `page-number ${i === currentPage ? 'active' : ''}`; - pageBtn.textContent = i; - pageBtn.addEventListener('click', () => { - currentPage = i; - loadCustomers(); - }); - pageNumbers.appendChild(pageBtn); - } - } - - // Create customer - createCustomerForm.addEventListener('submit', async function (e) { - e.preventDefault(); - - const formData = { - customerName: document.getElementById('createCustomerName').value, - intendedProduct: document.getElementById('createIntendedProduct').value, - version: document.getElementById('createVersion').value, - description: document.getElementById('createDescription').value, - solution: document.getElementById('createSolution').value, - type: document.getElementById('createType').value, - module: getModuleValue('createModule', 'createModuleOther'), - statusProgress: document.getElementById('createStatusProgress').value, - reporter: '' // Trial periods managed separately - }; - - try { - const response = await authenticatedFetch('/api/customers', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(formData) - }); - - if (response.ok) { - createCustomerForm.reset(); - // Hide module "other" input - const createModuleOther = document.getElementById('createModuleOther'); - if (createModuleOther) createModuleOther.style.display = 'none'; - createModal.style.display = 'none'; - loadCustomers(); - } else { - alert('创建客户时出错'); - } - } catch (error) { - console.error('Error creating customer:', error); - alert('创建客户时出错'); - } + customerTableBody.appendChild(row); }); - // Import customers - importFileForm.addEventListener('submit', async function (e) { - e.preventDefault(); + checkTextOverflow(); - const formData = new FormData(); - const fileInput = document.getElementById('importFile'); - - if (!fileInput.files.length) { - alert('请选择要导入的文件'); - return; - } - - formData.append('file', fileInput.files[0]); - - try { - const response = await authenticatedFetch('/api/customers/import', { - method: 'POST', - body: formData - }); - - if (response.ok) { - const result = await response.json(); - const message = `客户导入成功!\n导入数量: ${result.importedCount}\n重复数量: ${result.duplicateCount}`; - alert(message); - importFileForm.reset(); - fileName.textContent = '选择文件...'; - importModal.style.display = 'none'; - loadCustomers(); - } else { - alert('导入客户时出错'); - } - } catch (error) { - console.error('Error importing customers:', error); - alert('导入客户时出错'); - } + document.querySelectorAll(".edit-btn").forEach((btn) => { + btn.addEventListener("click", function () { + const customerId = this.getAttribute("data-id"); + openEditModal(customerId); + }); }); - // Open edit modal - async function openEditModal(customerId) { - try { - const response = await authenticatedFetch(`/api/customers/${customerId}`); - const customer = await response.json(); - - document.getElementById('editCustomerId').value = customer.id; - document.getElementById('editCustomerName').value = customer.customerName || ''; - document.getElementById('editIntendedProduct').value = normalizeDateValue(customer.intendedProduct); - document.getElementById('editVersion').value = customer.version || ''; - document.getElementById('editDescription').value = customer.description || ''; - document.getElementById('editSolution').value = customer.solution || ''; - document.getElementById('editType').value = customer.type || ''; - setModuleValue('editModule', 'editModuleOther', customer.module || ''); - document.getElementById('editStatusProgress').value = customer.statusProgress || ''; - - // Parse trial period and fill datetime inputs - const reporter = customer.reporter || ''; - if (reporter) { - // Split by ~ or ~ - const parts = reporter.split(/[~~]/); - if (parts.length === 2) { - const parseDateTime = (str) => { - // Parse format like "2026/1/5 10:00" or "2026-1-5 10:00" - const cleaned = str.trim(); - const match = cleaned.match(/(\d{4})[/-](\d{1,2})[/-](\d{1,2})\s+(\d{1,2}):(\d{1,2})/); - if (match) { - const [_, year, month, day, hours, minutes] = match; - return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; - } - return ''; - }; - - document.getElementById('editTrialStart').value = parseDateTime(parts[0]); - document.getElementById('editTrialEnd').value = parseDateTime(parts[1]); - } - } - - // Load trial periods for this customer - if (typeof loadTrialPeriods === 'function') { - await loadTrialPeriods(customer.id); - } - - editModal.style.display = 'block'; - } catch (error) { - console.error('Error loading customer for edit:', error); - } - } - - window.addEventListener('click', function (e) { - if (e.target === createModal) { - createModal.style.display = 'none'; - } - if (e.target === importModal) { - importModal.style.display = 'none'; - } - if (e.target === editModal) { - editModal.style.display = 'none'; - } + document.querySelectorAll(".delete-btn:not(.disabled)").forEach((btn) => { + btn.addEventListener("click", function () { + const customerId = this.getAttribute("data-id"); + deleteCustomer(customerId); + }); }); + } - // Update customer - editCustomerForm.addEventListener('submit', async function (e) { - e.preventDefault(); + function checkTextOverflow() { + // Use requestAnimationFrame to ensure DOM is fully rendered before checking overflow + requestAnimationFrame(() => { + const cells = customerTableBody.querySelectorAll("td[data-tooltip]"); + cells.forEach((cell) => { + const cellText = (cell.getAttribute("data-tooltip") || "").trim(); + const shouldAlwaysShowTooltip = + cell.classList.contains("overflow-cell") && cellText.length > 0; + const hasOverflow = cell.scrollWidth > cell.clientWidth; - const customerId = document.getElementById('editCustomerId').value; - - const formData = { - customerName: document.getElementById('editCustomerName').value, - intendedProduct: document.getElementById('editIntendedProduct').value, - version: document.getElementById('editVersion').value, - description: document.getElementById('editDescription').value, - solution: document.getElementById('editSolution').value, - type: document.getElementById('editType').value, - module: getModuleValue('editModule', 'editModuleOther'), - statusProgress: document.getElementById('editStatusProgress').value, - reporter: '' // Trial periods managed separately - }; - - try { - const response = await authenticatedFetch(`/api/customers/${customerId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(formData) - }); - - if (response.ok) { - editModal.style.display = 'none'; - loadCustomers(); - } else { - alert('更新客户时出错'); - } - } catch (error) { - console.error('Error updating customer:', error); - alert('更新客户时出错'); + if (shouldAlwaysShowTooltip || hasOverflow) { + cell.classList.add("has-overflow"); + } else { + cell.classList.remove("has-overflow"); } + }); }); + } - // Delete customer - async function deleteCustomer(customerId) { - if (!confirm('确定要删除这个客户吗?')) { - return; - } + // Update pagination controls + function updatePaginationControls() { + const startItem = totalItems === 0 ? 0 : (currentPage - 1) * pageSize + 1; + const endItem = Math.min(currentPage * pageSize, totalItems); - try { - const response = await authenticatedFetch(`/api/customers/${customerId}`, { - method: 'DELETE' - }); + document.getElementById("paginationInfo").textContent = + `显示 ${startItem}-${endItem} 共 ${totalItems} 条`; - if (response.ok) { - loadCustomers(); - } else { - alert('删除客户时出错'); - } - } catch (error) { - console.error('Error deleting customer:', error); - alert('删除客户时出错'); - } + document.getElementById("firstPage").disabled = currentPage === 1; + document.getElementById("prevPage").disabled = currentPage === 1; + document.getElementById("nextPage").disabled = currentPage === totalPages; + document.getElementById("lastPage").disabled = currentPage === totalPages; + + const pageNumbers = document.getElementById("pageNumbers"); + pageNumbers.innerHTML = ""; + + const maxVisiblePages = 5; + let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2)); + let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); + + if (endPage - startPage + 1 < maxVisiblePages) { + startPage = Math.max(1, endPage - maxVisiblePages + 1); } - // Dashboard functionality - async function loadDashboardData() { - await loadAllCustomers(); - await loadTrialCustomersForDashboard(); - await loadFollowUpCount(); - } - - // Load trial customers for dashboard statistics - async function loadTrialCustomersForDashboard() { - try { - const response = await authenticatedFetch('/api/trial-periods/all'); - const data = await response.json(); - const trialPeriods = data.trialPeriods || []; - - // Get customers map to resolve names - const customersResponse = await authenticatedFetch('/api/customers/list'); - const customersData = await customersResponse.json(); - const customersMap = customersData.customerMap || {}; - - // Collect unique trial customer names - directly use customerName field - const trialCustomerNames = new Set(); - trialPeriods.forEach(period => { - // 直接使用 customerName 字段 - if (period.customerName) { - trialCustomerNames.add(period.customerName); - } - }); - - // Update total customers display (数据仪表盘的总客户数从每周进度获取) - // This function will be called after loading dashboard customers - updateTotalCustomersWithTrialData(trialCustomerNames); - } catch (error) { - console.error('Error loading trial customers for dashboard:', error); - } - } - - // Update total customers count - 数据仪表盘的总客户数取的是每周进度表里的客户数量 - function updateTotalCustomersWithTrialData(trialCustomerNames) { - // Get current dashboard customers (from customers.json / 每周进度) - const progressCustomerNames = new Set(dashboardCustomers.map(c => c.customerName).filter(c => c)); - - // 数据仪表盘的总客户数取的是每周进度表里的客户数量,支持去重 - document.getElementById('totalCustomers').textContent = progressCustomerNames.size; - } - - // Update dashboard statistics - function updateDashboardStats(customers) { - const totalCustomers = new Set(customers.map(c => c.customerName).filter(c => c)).size; - - const now = new Date(); - const currentMonth = String(now.getMonth() + 1).padStart(2, '0'); - const currentYear = now.getFullYear(); - const newCustomers = new Set( - customers.filter(customer => { - if (!customer.customerName) return false; - const normalized = normalizeDateValue(customer.intendedProduct); - if (!normalized) return false; - const year = parseInt(normalized.slice(0, 4), 10); - const month = normalized.slice(5, 7); - return year === currentYear && month === currentMonth; - }).map(c => c.customerName).filter(c => c) - ).size; - - document.getElementById('totalCustomers').textContent = totalCustomers; - document.getElementById('newCustomers').textContent = newCustomers; - // 已成交数量将在 loadFollowUpCount 中更新 - } - - // Load follow-up count for dashboard - async function loadFollowUpCount() { - try { - const response = await authenticatedFetch('/api/followups?page=1&pageSize=1000'); - const data = await response.json(); - - if (data.followUps) { - const followUpCount = data.followUps.length; - document.getElementById('followUpCount').textContent = followUpCount; - - // 计算已成交数量:统计 dealStatus 为"已成交"的唯一客户数 - const completedCustomers = new Set( - data.followUps - .filter(f => f.dealStatus === '已成交') - .map(f => f.customerName) - .filter(name => name) - ).size; - document.getElementById('completedTasks').textContent = completedCustomers; - } else { - document.getElementById('followUpCount').textContent = '0'; - document.getElementById('completedTasks').textContent = '0'; - } - } catch (error) { - console.error('Error loading follow-up count:', error); - document.getElementById('followUpCount').textContent = '0'; - document.getElementById('completedTasks').textContent = '0'; - } - } - - function renderStatusChart(customers) { - const ctx = document.getElementById('statusChart').getContext('2d'); - const selectedField = document.getElementById('chartFieldSelect').value; - const chartTitle = document.getElementById('statusChartTitle').value || '数据分布'; - - const fieldCount = {}; - customers.forEach(customer => { - const value = customer[selectedField] || '未设置'; - fieldCount[value] = (fieldCount[value] || 0) + 1; - }); - - const labels = Object.keys(fieldCount); - const data = Object.values(fieldCount); - - if (statusChartInstance) { - statusChartInstance.destroy(); - } - - statusChartInstance = new Chart(ctx, { - type: 'pie', - data: { - labels: labels, - datasets: [{ - data: data, - backgroundColor: [ - '#FF6B35', - '#F28C28', - '#333', - '#4CAF50', - '#2196F3', - '#FFC107', - '#9C27B0', - '#00BCD4', - '#8BC34A', - '#FF5722' - ] - }] - }, - options: { - responsive: true, - plugins: { - legend: { - position: 'left', - labels: { - generateLabels: function (chart) { - const data = chart.data; - if (data.labels.length && data.datasets.length) { - return data.labels.map((label, i) => { - const value = data.datasets[0].data[i]; - return { - text: `${label}: ${value}`, - fillStyle: data.datasets[0].backgroundColor[i], - hidden: false, - index: i - }; - }); - } - return []; - } - } - }, - title: { - display: true, - text: chartTitle, - font: { - size: 16, - weight: 'bold' - } - }, - tooltip: { - callbacks: { - label: function (context) { - const label = context.label || ''; - const value = context.parsed; - const total = context.dataset.data.reduce((a, b) => a + b, 0); - const percentage = ((value / total) * 100).toFixed(1); - return `${label}: ${value} (${percentage}%)`; - } - } - }, - datalabels: { - formatter: (value, ctx) => { - const total = ctx.dataset.data.reduce((a, b) => a + b, 0); - const percentage = ((value / total) * 100).toFixed(1); - // 显示具体数值和百分比 - return `${value}\n(${percentage}%)`; - }, - color: '#fff', - font: { - weight: 'bold', - size: 12 - } - } - } - } - }); - } - - // Render customer type chart - function renderTypeChart(customers) { - const canvas = document.getElementById('typeChart'); - - if (!canvas) { - console.error('typeChart canvas not found'); - return; - } - - const ctx = canvas.getContext('2d'); - const chartTitle = document.getElementById('typeChartTitle').value || '客户类型'; - - if (typeChartInstance) { - typeChartInstance.destroy(); - } - - const selectedField = document.getElementById('typeChartFieldSelect').value; - const typeCount = {}; - customers.forEach(customer => { - const type = customer[selectedField] || '未设置'; - typeCount[type] = (typeCount[type] || 0) + 1; - }); - - const labels = Object.keys(typeCount); - const data = Object.values(typeCount); - - typeChartInstance = new Chart(ctx, { - type: 'doughnut', - data: { - labels: labels, - datasets: [{ - data: data, - backgroundColor: [ - '#333', - '#FF6B35', - '#F28C28', - '#4CAF50', - '#2196F3', - '#FFC107' - ] - }] - }, - options: { - responsive: true, - plugins: { - legend: { - position: 'left', - labels: { - generateLabels: function (chart) { - const data = chart.data; - if (data.labels.length && data.datasets.length) { - return data.labels.map((label, i) => { - const value = data.datasets[0].data[i]; - return { - text: `${label}: ${value}`, - fillStyle: data.datasets[0].backgroundColor[i], - hidden: false, - index: i - }; - }); - } - return []; - } - } - }, - title: { - display: true, - text: chartTitle, - font: { - size: 16, - weight: 'bold' - } - }, - tooltip: { - callbacks: { - label: function (context) { - const label = context.label || ''; - const value = context.parsed; - const total = context.dataset.data.reduce((a, b) => a + b, 0); - const percentage = ((value / total) * 100).toFixed(1); - return `${label}: ${value} (${percentage}%)`; - } - } - }, - datalabels: { - formatter: (value, ctx) => { - const total = ctx.dataset.data.reduce((a, b) => a + b, 0); - const percentage = ((value / total) * 100).toFixed(1); - // 显示具体数值和百分比 - return `${value}\n(${percentage}%)`; - }, - color: '#fff', - font: { - weight: 'bold', - size: 12 - } - } - } - } - }); - } - - // Render trend line chart - function renderTrendChart(customers) { - const canvas = document.getElementById('trendChart'); - if (!canvas) { - console.error('trendChart canvas not found'); - return; - } - - const ctx = canvas.getContext('2d'); - const trendType = document.getElementById('trendTypeSelect')?.value || 'customer'; - - if (trendChartInstance) { - trendChartInstance.destroy(); - } - - // Group data by date - const dateMap = {}; - - customers.forEach(customer => { - const dateStr = normalizeDateValue(customer.intendedProduct); - if (!dateStr) return; - - if (!dateMap[dateStr]) { - dateMap[dateStr] = { - customers: new Set(), - demands: 0, - issues: 0 - }; - } - - // Count customers - if (customer.customerName) { - dateMap[dateStr].customers.add(customer.customerName); - } - - // Count demands and issues based on type - const type = (customer.type || '').toLowerCase(); - if (type.includes('需求')) { - dateMap[dateStr].demands++; - } - if (type.includes('问题') || type.includes('功能问题')) { - dateMap[dateStr].issues++; - } - }); - - // Sort dates - const sortedDates = Object.keys(dateMap).sort(); - - // Prepare datasets based on selected trend type - const datasets = []; - - if (trendType === 'customer') { - datasets.push({ - label: '客户数', - data: sortedDates.map(date => dateMap[date].customers.size), - borderColor: '#FF6B35', - backgroundColor: 'rgba(255, 107, 53, 0.1)', - borderWidth: 3, - pointRadius: 5, - pointHoverRadius: 7, - pointBackgroundColor: '#FF6B35', - pointBorderColor: '#fff', - pointBorderWidth: 2, - tension: 0.4, - fill: true - }); - } else if (trendType === 'demand') { - datasets.push({ - label: '需求数', - data: sortedDates.map(date => dateMap[date].demands), - borderColor: '#4CAF50', - backgroundColor: 'rgba(76, 175, 80, 0.1)', - borderWidth: 3, - pointRadius: 5, - pointHoverRadius: 7, - pointBackgroundColor: '#4CAF50', - pointBorderColor: '#fff', - pointBorderWidth: 2, - tension: 0.4, - fill: true - }); - } else if (trendType === 'issue') { - datasets.push({ - label: '问题数', - data: sortedDates.map(date => dateMap[date].issues), - borderColor: '#F28C28', - backgroundColor: 'rgba(242, 140, 40, 0.1)', - borderWidth: 3, - pointRadius: 5, - pointHoverRadius: 7, - pointBackgroundColor: '#F28C28', - pointBorderColor: '#fff', - pointBorderWidth: 2, - tension: 0.4, - fill: true - }); - } else if (trendType === 'all') { - datasets.push( - { - label: '客户数', - data: sortedDates.map(date => dateMap[date].customers.size), - borderColor: '#FF6B35', - backgroundColor: 'rgba(255, 107, 53, 0.1)', - borderWidth: 3, - pointRadius: 5, - pointHoverRadius: 7, - pointBackgroundColor: '#FF6B35', - pointBorderColor: '#fff', - pointBorderWidth: 2, - tension: 0.4, - fill: false - }, - { - label: '需求数', - data: sortedDates.map(date => dateMap[date].demands), - borderColor: '#4CAF50', - backgroundColor: 'rgba(76, 175, 80, 0.1)', - borderWidth: 3, - pointRadius: 5, - pointHoverRadius: 7, - pointBackgroundColor: '#4CAF50', - pointBorderColor: '#fff', - pointBorderWidth: 2, - tension: 0.4, - fill: false - }, - { - label: '问题数', - data: sortedDates.map(date => dateMap[date].issues), - borderColor: '#F28C28', - backgroundColor: 'rgba(242, 140, 40, 0.1)', - borderWidth: 3, - pointRadius: 5, - pointHoverRadius: 7, - pointBackgroundColor: '#F28C28', - pointBorderColor: '#fff', - pointBorderWidth: 2, - tension: 0.4, - fill: false - } - ); - } - - trendChartInstance = new Chart(ctx, { - type: 'line', - data: { - labels: sortedDates, - datasets: datasets - }, - options: { - responsive: true, - maintainAspectRatio: true, - plugins: { - legend: { - position: 'top' - }, - title: { - display: true, - text: '时间趋势分析', - font: { - size: 16, - weight: 'bold' - } - }, - tooltip: { - mode: 'index', - intersect: false - }, - datalabels: { - display: true, - align: 'top', - anchor: 'end', - formatter: (value) => value, - color: '#333', - font: { - weight: 'bold', - size: 11 - }, - backgroundColor: 'rgba(255, 255, 255, 0.8)', - borderRadius: 4, - padding: 4 - } - }, - scales: { - x: { - display: true, - title: { - display: true, - text: '日期' - }, - ticks: { - maxRotation: 45, - minRotation: 45 - } - }, - y: { - display: true, - title: { - display: true, - text: '数量' - }, - beginAtZero: true, - ticks: { - stepSize: 1 - } - } - }, - interaction: { - mode: 'nearest', - axis: 'x', - intersect: false - } - } - }); - } - - // Initialize the app - loadCustomers().then(() => { - loadDashboardData(); // Changed from loadAllCustomers() to include trial customers - }); - - // Pagination event listeners - document.getElementById('firstPage').addEventListener('click', () => { - if (currentPage > 1) { - currentPage = 1; - applyCustomerFilter(); - } - }); - - document.getElementById('prevPage').addEventListener('click', () => { - if (currentPage > 1) { - currentPage--; - applyCustomerFilter(); - } - }); - - document.getElementById('nextPage').addEventListener('click', () => { - if (currentPage < totalPages) { - currentPage++; - applyCustomerFilter(); - } - }); - - document.getElementById('lastPage').addEventListener('click', () => { - if (currentPage < totalPages) { - currentPage = totalPages; - applyCustomerFilter(); - } - }); - - document.getElementById('pageSizeSelect').addEventListener('change', (e) => { - pageSize = parseInt(e.target.value); - currentPage = 1; + for (let i = startPage; i <= endPage; i++) { + const pageBtn = document.createElement("button"); + pageBtn.className = `page-number ${i === currentPage ? "active" : ""}`; + pageBtn.textContent = i; + pageBtn.addEventListener("click", () => { + currentPage = i; loadCustomers(); - }); + }); + pageNumbers.appendChild(pageBtn); + } + } - // Chart field select event listener - document.getElementById('chartFieldSelect').addEventListener('change', function () { - const startDate = document.getElementById('startDate').value; - const endDate = document.getElementById('endDate').value; - applyDateFilter(startDate, endDate); - }); + // Create customer + createCustomerForm.addEventListener("submit", async function (e) { + e.preventDefault(); - // Type chart field select event listener - document.getElementById('typeChartFieldSelect').addEventListener('change', function () { - const startDate = document.getElementById('startDate').value; - const endDate = document.getElementById('endDate').value; - applyDateFilter(startDate, endDate); - }); + const formData = { + customerName: document.getElementById("createCustomerName").value, + intendedProduct: document.getElementById("createIntendedProduct").value, + version: document.getElementById("createVersion").value, + description: document.getElementById("createDescription").value, + solution: document.getElementById("createSolution").value, + type: document.getElementById("createType").value, + module: getModuleValue("createModule", "createModuleOther"), + statusProgress: document.getElementById("createStatusProgress").value, + reporter: "", // Trial periods managed separately + }; - // Trend type select event listener - const trendTypeSelect = document.getElementById('trendTypeSelect'); - if (trendTypeSelect) { - trendTypeSelect.addEventListener('change', function () { - const startDate = document.getElementById('startDate').value; - const endDate = document.getElementById('endDate').value; - applyDateFilter(startDate, endDate); - }); + try { + const response = await authenticatedFetch("/api/customers", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + if (response.ok) { + createCustomerForm.reset(); + // Hide module "other" input + const createModuleOther = document.getElementById("createModuleOther"); + if (createModuleOther) createModuleOther.style.display = "none"; + createModal.style.display = "none"; + loadCustomers(); + } else { + alert("创建客户时出错"); + } + } catch (error) { + console.error("Error creating customer:", error); + alert("创建客户时出错"); + } + }); + + // Import customers + importFileForm.addEventListener("submit", async function (e) { + e.preventDefault(); + + const formData = new FormData(); + const fileInput = document.getElementById("importFile"); + + if (!fileInput.files.length) { + alert("请选择要导入的文件"); + return; } - // ========== Follow-up Management ========== - const followupSection = document.getElementById('followupSection'); - const addFollowUpBtn = document.getElementById('addFollowUpBtn'); - const followupFormCard = document.getElementById('followupFormCard'); - const followupForm = document.getElementById('followupForm'); - const cancelFollowupBtn = document.getElementById('cancelFollowupBtn'); - const followupTableBody = document.getElementById('followupTableBody'); - const refreshFollowupsBtn = document.getElementById('refreshFollowupsBtn'); - const followupCustomerNameSelect = document.getElementById('followupCustomerName'); + formData.append("file", fileInput.files[0]); - let allFollowUps = []; - let followupCurrentPage = 1; - let followupPageSize = 10; - let followupTotalPages = 1; - let followupTotalItems = 0; + try { + const response = await authenticatedFetch("/api/customers/import", { + method: "POST", + body: formData, + }); - // 弹窗相关元素 - const addFollowupModal = document.getElementById('addFollowupModal'); - const addFollowupModalForm = document.getElementById('addFollowupModalForm'); - const editFollowupModal = document.getElementById('editFollowupModal'); - const editFollowupForm = document.getElementById('editFollowupForm'); - - // 点击添加跟进按钮 - 弹出弹窗 - if (addFollowUpBtn) { - addFollowUpBtn.addEventListener('click', async function () { - await loadCustomerListForFollowupModal(); - addFollowupModal.style.display = 'block'; - }); + if (response.ok) { + const result = await response.json(); + const message = `客户导入成功!\n导入数量: ${result.importedCount}\n重复数量: ${result.duplicateCount}`; + alert(message); + importFileForm.reset(); + fileName.textContent = "选择文件..."; + importModal.style.display = "none"; + loadCustomers(); + } else { + alert("导入客户时出错"); + } + } catch (error) { + console.error("Error importing customers:", error); + alert("导入客户时出错"); } + }); - // 取消添加跟进弹窗 - if (addFollowupModal) { - addFollowupModal.querySelector('.close').addEventListener('click', () => { - addFollowupModal.style.display = 'none'; - addFollowupModalForm.reset(); - }); - addFollowupModal.querySelector('.cancel-add-followup').addEventListener('click', () => { - addFollowupModal.style.display = 'none'; - addFollowupModalForm.reset(); - }); - } + // Open edit modal + async function openEditModal(customerId) { + try { + const response = await authenticatedFetch(`/api/customers/${customerId}`); + const customer = await response.json(); - // 取消编辑跟进弹窗 - if (editFollowupModal) { - editFollowupModal.querySelector('.close').addEventListener('click', () => { - editFollowupModal.style.display = 'none'; - editFollowupForm.reset(); - }); - editFollowupModal.querySelector('.cancel-edit-followup').addEventListener('click', () => { - editFollowupModal.style.display = 'none'; - editFollowupForm.reset(); - }); - } + document.getElementById("editCustomerId").value = customer.id; + document.getElementById("editCustomerName").value = + customer.customerName || ""; + document.getElementById("editIntendedProduct").value = normalizeDateValue( + customer.intendedProduct, + ); + document.getElementById("editVersion").value = customer.version || ""; + document.getElementById("editDescription").value = + customer.description || ""; + document.getElementById("editSolution").value = customer.solution || ""; + document.getElementById("editType").value = customer.type || ""; + setModuleValue("editModule", "editModuleOther", customer.module || ""); + document.getElementById("editStatusProgress").value = + customer.statusProgress || ""; - // 旧表单相关逻辑保留兼容 - if (cancelFollowupBtn) { - cancelFollowupBtn.addEventListener('click', function () { - followupFormCard.style.display = 'none'; - followupForm.reset(); - }); - } - - // Load customer list for follow-up dropdown from trial periods (for modal) - async function loadCustomerListForFollowupModal(selectId = 'addFollowupCustomerName') { - try { - const response = await authenticatedFetch('/api/trial-customers/list'); - const data = await response.json(); - - const selectElement = document.getElementById(selectId); - if (!selectElement) return; - - selectElement.innerHTML = '请选择客户'; - if (data.customerNames && data.customerNames.length > 0) { - data.customerNames.forEach(customerName => { - const option = document.createElement('option'); - option.value = customerName; - option.textContent = customerName; - selectElement.appendChild(option); - }); + // Parse trial period and fill datetime inputs + const reporter = customer.reporter || ""; + if (reporter) { + // Split by ~ or ~ + const parts = reporter.split(/[~~]/); + if (parts.length === 2) { + const parseDateTime = (str) => { + // Parse format like "2026/1/5 10:00" or "2026-1-5 10:00" + const cleaned = str.trim(); + const match = cleaned.match( + /(\d{4})[/-](\d{1,2})[/-](\d{1,2})\s+(\d{1,2}):(\d{1,2})/, + ); + if (match) { + const [_, year, month, day, hours, minutes] = match; + return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}T${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`; } - } catch (error) { - console.error('Error loading customer list:', error); + return ""; + }; + + document.getElementById("editTrialStart").value = parseDateTime( + parts[0], + ); + document.getElementById("editTrialEnd").value = parseDateTime( + parts[1], + ); } + } + + // Load trial periods for this customer + if (typeof loadTrialPeriods === "function") { + await loadTrialPeriods(customer.id); + } + + editModal.style.display = "block"; + } catch (error) { + console.error("Error loading customer for edit:", error); + } + } + + window.addEventListener("click", function (e) { + if (e.target === createModal) { + createModal.style.display = "none"; + } + if (e.target === importModal) { + importModal.style.display = "none"; + } + if (e.target === editModal) { + editModal.style.display = "none"; + } + }); + + // Update customer + editCustomerForm.addEventListener("submit", async function (e) { + e.preventDefault(); + + const customerId = document.getElementById("editCustomerId").value; + + const formData = { + customerName: document.getElementById("editCustomerName").value, + intendedProduct: document.getElementById("editIntendedProduct").value, + version: document.getElementById("editVersion").value, + description: document.getElementById("editDescription").value, + solution: document.getElementById("editSolution").value, + type: document.getElementById("editType").value, + module: getModuleValue("editModule", "editModuleOther"), + statusProgress: document.getElementById("editStatusProgress").value, + reporter: "", // Trial periods managed separately + }; + + try { + const response = await authenticatedFetch( + `/api/customers/${customerId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }, + ); + + if (response.ok) { + editModal.style.display = "none"; + loadCustomers(); + } else { + alert("更新客户时出错"); + } + } catch (error) { + console.error("Error updating customer:", error); + alert("更新客户时出错"); + } + }); + + // Delete customer + async function deleteCustomer(customerId) { + if (!confirm("确定要删除这个客户吗?")) { + return; } - // Create follow-up - if (followupForm) { - followupForm.addEventListener('submit', async function (e) { - e.preventDefault(); + try { + const response = await authenticatedFetch( + `/api/customers/${customerId}`, + { + method: "DELETE", + }, + ); - const followUpTimeValue = document.getElementById('followupTime').value; - const followUpTimeISO = new Date(followUpTimeValue).toISOString(); + if (response.ok) { + loadCustomers(); + } else { + alert("删除客户时出错"); + } + } catch (error) { + console.error("Error deleting customer:", error); + alert("删除客户时出错"); + } + } - // Get customer name from the selected option's text - const customerSelect = document.getElementById('followupCustomerName'); - const selectedOption = customerSelect.options[customerSelect.selectedIndex]; - const customerName = selectedOption ? selectedOption.textContent : ''; + // Dashboard functionality + async function loadDashboardData() { + await loadAllCustomers(); + await loadTrialCustomersForDashboard(); + await loadFollowUpCount(); + } - const formData = { - customerName: customerName, - dealStatus: document.getElementById('followupDealStatus').value, - customerLevel: document.getElementById('followupCustomerLevel').value, - industry: document.getElementById('followupIndustry').value, - followUpTime: followUpTimeISO - }; + // Load trial customers for dashboard statistics + async function loadTrialCustomersForDashboard() { + try { + const response = await authenticatedFetch("/api/trial-periods/all"); + const data = await response.json(); + const trialPeriods = data.trialPeriods || []; - try { - const response = await authenticatedFetch('/api/followups', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(formData) - }); + // Get customers map to resolve names + const customersResponse = await authenticatedFetch("/api/customers/list"); + const customersData = await customersResponse.json(); + const customersMap = customersData.customerMap || {}; - if (response.ok) { - followupForm.reset(); - followupFormCard.style.display = 'none'; - loadFollowUps(); - } else { - alert('创建跟进记录时出错'); + // Collect unique trial customer names - directly use customerName field + const trialCustomerNames = new Set(); + trialPeriods.forEach((period) => { + // 直接使用 customerName 字段 + if (period.customerName) { + trialCustomerNames.add(period.customerName); + } + }); + + // Update total customers display (数据仪表盘的总客户数从每周进度获取) + // This function will be called after loading dashboard customers + updateTotalCustomersWithTrialData(trialCustomerNames); + } catch (error) { + console.error("Error loading trial customers for dashboard:", error); + } + } + + // Update total customers count - 数据仪表盘的总客户数取的是每周进度表里的客户数量 + function updateTotalCustomersWithTrialData(trialCustomerNames) { + // Get current dashboard customers (from customers.json / 每周进度) + const progressCustomerNames = new Set( + dashboardCustomers.map((c) => c.customerName).filter((c) => c), + ); + + // 数据仪表盘的总客户数取的是每周进度表里的客户数量,支持去重 + document.getElementById("totalCustomers").textContent = + progressCustomerNames.size; + } + + // Update dashboard statistics + function updateDashboardStats(customers) { + const totalCustomers = new Set( + customers.map((c) => c.customerName).filter((c) => c), + ).size; + + const now = new Date(); + const currentMonth = String(now.getMonth() + 1).padStart(2, "0"); + const currentYear = now.getFullYear(); + const newCustomers = new Set( + customers + .filter((customer) => { + if (!customer.customerName) return false; + const normalized = normalizeDateValue(customer.intendedProduct); + if (!normalized) return false; + const year = parseInt(normalized.slice(0, 4), 10); + const month = normalized.slice(5, 7); + return year === currentYear && month === currentMonth; + }) + .map((c) => c.customerName) + .filter((c) => c), + ).size; + + document.getElementById("totalCustomers").textContent = totalCustomers; + document.getElementById("newCustomers").textContent = newCustomers; + // 已成交数量将在 loadFollowUpCount 中更新 + } + + // Load follow-up count for dashboard + async function loadFollowUpCount() { + try { + const response = await authenticatedFetch( + "/api/followups?page=1&pageSize=1000", + ); + const data = await response.json(); + + if (data.followUps) { + const followUpCount = data.followUps.length; + document.getElementById("followUpCount").textContent = followUpCount; + + // 计算已成交数量:统计 dealStatus 为"已成交"的唯一客户数 + const completedCustomers = new Set( + data.followUps + .filter((f) => f.dealStatus === "已成交") + .map((f) => f.customerName) + .filter((name) => name), + ).size; + document.getElementById("completedTasks").textContent = + completedCustomers; + } else { + document.getElementById("followUpCount").textContent = "0"; + document.getElementById("completedTasks").textContent = "0"; + } + } catch (error) { + console.error("Error loading follow-up count:", error); + document.getElementById("followUpCount").textContent = "0"; + document.getElementById("completedTasks").textContent = "0"; + } + } + + function renderStatusChart(customers) { + const ctx = document.getElementById("statusChart").getContext("2d"); + const selectedField = document.getElementById("chartFieldSelect").value; + const statusChartTitleEl = document.getElementById("statusChartTitle"); + const chartTitle = statusChartTitleEl ? statusChartTitleEl.value : "数据分布"; + + const fieldCount = {}; + customers.forEach((customer) => { + const value = customer[selectedField] || "未设置"; + fieldCount[value] = (fieldCount[value] || 0) + 1; + }); + + const labels = Object.keys(fieldCount); + const data = Object.values(fieldCount); + + if (statusChartInstance) { + statusChartInstance.destroy(); + } + + statusChartInstance = new Chart(ctx, { + type: "pie", + data: { + labels: labels, + datasets: [ + { + data: data, + backgroundColor: [ + "#FF6B35", + "#F28C28", + "#333", + "#4CAF50", + "#2196F3", + "#FFC107", + "#9C27B0", + "#00BCD4", + "#8BC34A", + "#FF5722", + ], + }, + ], + }, + options: { + responsive: true, + plugins: { + legend: { + position: "left", + labels: { + generateLabels: function (chart) { + const data = chart.data; + if (data.labels.length && data.datasets.length) { + return data.labels.map((label, i) => { + const value = data.datasets[0].data[i]; + return { + text: `${label}: ${value}`, + fillStyle: data.datasets[0].backgroundColor[i], + hidden: false, + index: i, + }; + }); } - } catch (error) { - console.error('Error creating follow-up:', error); - alert('创建跟进记录时出错'); - } + return []; + }, + }, + }, + title: { + display: true, + text: chartTitle, + font: { + size: 16, + weight: "bold", + }, + }, + tooltip: { + callbacks: { + label: function (context) { + const label = context.label || ""; + const value = context.parsed; + const total = context.dataset.data.reduce((a, b) => a + b, 0); + const percentage = ((value / total) * 100).toFixed(1); + return `${label}: ${value} (${percentage}%)`; + }, + }, + }, + datalabels: { + formatter: (value, ctx) => { + const total = ctx.dataset.data.reduce((a, b) => a + b, 0); + const percentage = ((value / total) * 100).toFixed(1); + // 显示具体数值和百分比 + return `${value}\n(${percentage}%)`; + }, + color: "#fff", + font: { + weight: "bold", + size: 12, + }, + }, + }, + }, + }); + } + + // Render customer type chart + function renderTypeChart(customers) { + const canvas = document.getElementById("typeChart"); + + if (!canvas) { + console.error("typeChart canvas not found"); + return; + } + + const ctx = canvas.getContext("2d"); + const typeChartTitleEl = document.getElementById("typeChartTitle"); + const chartTitle = typeChartTitleEl ? typeChartTitleEl.value : "客户类型"; + + if (typeChartInstance) { + typeChartInstance.destroy(); + } + + const selectedField = document.getElementById("typeChartFieldSelect").value; + const typeCount = {}; + customers.forEach((customer) => { + const type = customer[selectedField] || "未设置"; + typeCount[type] = (typeCount[type] || 0) + 1; + }); + + const labels = Object.keys(typeCount); + const data = Object.values(typeCount); + + typeChartInstance = new Chart(ctx, { + type: "doughnut", + data: { + labels: labels, + datasets: [ + { + data: data, + backgroundColor: [ + "#333", + "#FF6B35", + "#F28C28", + "#4CAF50", + "#2196F3", + "#FFC107", + ], + }, + ], + }, + options: { + responsive: true, + plugins: { + legend: { + position: "left", + labels: { + generateLabels: function (chart) { + const data = chart.data; + if (data.labels.length && data.datasets.length) { + return data.labels.map((label, i) => { + const value = data.datasets[0].data[i]; + return { + text: `${label}: ${value}`, + fillStyle: data.datasets[0].backgroundColor[i], + hidden: false, + index: i, + }; + }); + } + return []; + }, + }, + }, + title: { + display: true, + text: chartTitle, + font: { + size: 16, + weight: "bold", + }, + }, + tooltip: { + callbacks: { + label: function (context) { + const label = context.label || ""; + const value = context.parsed; + const total = context.dataset.data.reduce((a, b) => a + b, 0); + const percentage = ((value / total) * 100).toFixed(1); + return `${label}: ${value} (${percentage}%)`; + }, + }, + }, + datalabels: { + formatter: (value, ctx) => { + const total = ctx.dataset.data.reduce((a, b) => a + b, 0); + const percentage = ((value / total) * 100).toFixed(1); + // 显示具体数值和百分比 + return `${value}\n(${percentage}%)`; + }, + color: "#fff", + font: { + weight: "bold", + size: 12, + }, + }, + }, + }, + }); + } + + // Render trend line chart + function renderTrendChart(customers) { + const canvas = document.getElementById("trendChart"); + if (!canvas) { + console.error("trendChart canvas not found"); + return; + } + + const ctx = canvas.getContext("2d"); + const trendType = + document.getElementById("trendTypeSelect")?.value || "customer"; + + if (trendChartInstance) { + trendChartInstance.destroy(); + } + + // Group data by date + const dateMap = {}; + + customers.forEach((customer) => { + const dateStr = normalizeDateValue(customer.intendedProduct); + if (!dateStr) return; + + if (!dateMap[dateStr]) { + dateMap[dateStr] = { + customers: new Set(), + demands: 0, + issues: 0, + }; + } + + // Count customers + if (customer.customerName) { + dateMap[dateStr].customers.add(customer.customerName); + } + + // Count demands and issues based on type + const type = (customer.type || "").toLowerCase(); + if (type.includes("需求")) { + dateMap[dateStr].demands++; + } + if (type.includes("问题") || type.includes("功能问题")) { + dateMap[dateStr].issues++; + } + }); + + // Sort dates + const sortedDates = Object.keys(dateMap).sort(); + + // Prepare datasets based on selected trend type + const datasets = []; + + if (trendType === "customer") { + datasets.push({ + label: "客户数", + data: sortedDates.map((date) => dateMap[date].customers.size), + borderColor: "#FF6B35", + backgroundColor: "rgba(255, 107, 53, 0.1)", + borderWidth: 3, + pointRadius: 5, + pointHoverRadius: 7, + pointBackgroundColor: "#FF6B35", + pointBorderColor: "#fff", + pointBorderWidth: 2, + tension: 0.4, + fill: true, + }); + } else if (trendType === "demand") { + datasets.push({ + label: "需求数", + data: sortedDates.map((date) => dateMap[date].demands), + borderColor: "#4CAF50", + backgroundColor: "rgba(76, 175, 80, 0.1)", + borderWidth: 3, + pointRadius: 5, + pointHoverRadius: 7, + pointBackgroundColor: "#4CAF50", + pointBorderColor: "#fff", + pointBorderWidth: 2, + tension: 0.4, + fill: true, + }); + } else if (trendType === "issue") { + datasets.push({ + label: "问题数", + data: sortedDates.map((date) => dateMap[date].issues), + borderColor: "#F28C28", + backgroundColor: "rgba(242, 140, 40, 0.1)", + borderWidth: 3, + pointRadius: 5, + pointHoverRadius: 7, + pointBackgroundColor: "#F28C28", + pointBorderColor: "#fff", + pointBorderWidth: 2, + tension: 0.4, + fill: true, + }); + } else if (trendType === "all") { + datasets.push( + { + label: "客户数", + data: sortedDates.map((date) => dateMap[date].customers.size), + borderColor: "#FF6B35", + backgroundColor: "rgba(255, 107, 53, 0.1)", + borderWidth: 3, + pointRadius: 5, + pointHoverRadius: 7, + pointBackgroundColor: "#FF6B35", + pointBorderColor: "#fff", + pointBorderWidth: 2, + tension: 0.4, + fill: false, + }, + { + label: "需求数", + data: sortedDates.map((date) => dateMap[date].demands), + borderColor: "#4CAF50", + backgroundColor: "rgba(76, 175, 80, 0.1)", + borderWidth: 3, + pointRadius: 5, + pointHoverRadius: 7, + pointBackgroundColor: "#4CAF50", + pointBorderColor: "#fff", + pointBorderWidth: 2, + tension: 0.4, + fill: false, + }, + { + label: "问题数", + data: sortedDates.map((date) => dateMap[date].issues), + borderColor: "#F28C28", + backgroundColor: "rgba(242, 140, 40, 0.1)", + borderWidth: 3, + pointRadius: 5, + pointHoverRadius: 7, + pointBackgroundColor: "#F28C28", + pointBorderColor: "#fff", + pointBorderWidth: 2, + tension: 0.4, + fill: false, + }, + ); + } + + trendChartInstance = new Chart(ctx, { + type: "line", + data: { + labels: sortedDates, + datasets: datasets, + }, + options: { + responsive: true, + maintainAspectRatio: true, + plugins: { + legend: { + position: "top", + }, + title: { + display: true, + text: "时间趋势分析", + font: { + size: 16, + weight: "bold", + }, + }, + tooltip: { + mode: "index", + intersect: false, + }, + datalabels: { + display: true, + align: "top", + anchor: "end", + formatter: (value) => value, + color: "#333", + font: { + weight: "bold", + size: 11, + }, + backgroundColor: "rgba(255, 255, 255, 0.8)", + borderRadius: 4, + padding: 4, + }, + }, + scales: { + x: { + display: true, + title: { + display: true, + text: "日期", + }, + ticks: { + maxRotation: 45, + minRotation: 45, + }, + }, + y: { + display: true, + title: { + display: true, + text: "数量", + }, + beginAtZero: true, + ticks: { + stepSize: 1, + }, + }, + }, + interaction: { + mode: "nearest", + axis: "x", + intersect: false, + }, + }, + }); + } + + // Initialize the app + loadCustomers().then(() => { + loadDashboardData(); // Changed from loadAllCustomers() to include trial customers + }); + + // Pagination event listeners + document.getElementById("firstPage").addEventListener("click", () => { + if (currentPage > 1) { + currentPage = 1; + applyCustomerFilter(); + } + }); + + document.getElementById("prevPage").addEventListener("click", () => { + if (currentPage > 1) { + currentPage--; + applyCustomerFilter(); + } + }); + + document.getElementById("nextPage").addEventListener("click", () => { + if (currentPage < totalPages) { + currentPage++; + applyCustomerFilter(); + } + }); + + document.getElementById("lastPage").addEventListener("click", () => { + if (currentPage < totalPages) { + currentPage = totalPages; + applyCustomerFilter(); + } + }); + + document.getElementById("pageSizeSelect").addEventListener("change", (e) => { + pageSize = parseInt(e.target.value); + currentPage = 1; + loadCustomers(); + }); + + // Chart field select event listener + document + .getElementById("chartFieldSelect") + .addEventListener("change", function () { + const startDate = document.getElementById("startDate").value; + const endDate = document.getElementById("endDate").value; + applyDateFilter(startDate, endDate); + }); + + // Type chart field select event listener + document + .getElementById("typeChartFieldSelect") + .addEventListener("change", function () { + const startDate = document.getElementById("startDate").value; + const endDate = document.getElementById("endDate").value; + applyDateFilter(startDate, endDate); + }); + + // Trend type select event listener + const trendTypeSelect = document.getElementById("trendTypeSelect"); + if (trendTypeSelect) { + trendTypeSelect.addEventListener("change", function () { + const startDate = document.getElementById("startDate").value; + const endDate = document.getElementById("endDate").value; + applyDateFilter(startDate, endDate); + }); + } + + // ========== Follow-up Management ========== + const followupSection = document.getElementById("followupSection"); + const addFollowUpBtn = document.getElementById("addFollowUpBtn"); + const followupFormCard = document.getElementById("followupFormCard"); + const followupForm = document.getElementById("followupForm"); + const cancelFollowupBtn = document.getElementById("cancelFollowupBtn"); + const followupTableBody = document.getElementById("followupTableBody"); + const refreshFollowupsBtn = document.getElementById("refreshFollowupsBtn"); + const followupCustomerNameSelect = document.getElementById( + "followupCustomerName", + ); + + let allFollowUps = []; + let followupCurrentPage = 1; + let followupPageSize = 10; + let followupTotalPages = 1; + let followupTotalItems = 0; + + // 弹窗相关元素 + const addFollowupModal = document.getElementById("addFollowupModal"); + const addFollowupModalForm = document.getElementById("addFollowupModalForm"); + const editFollowupModal = document.getElementById("editFollowupModal"); + const editFollowupForm = document.getElementById("editFollowupForm"); + + // 点击添加跟进按钮 - 弹出弹窗 + if (addFollowUpBtn) { + addFollowUpBtn.addEventListener("click", async function () { + await loadCustomerListForFollowupModal(); + addFollowupModal.style.display = "block"; + }); + } + + // 取消添加跟进弹窗 + if (addFollowupModal) { + addFollowupModal.querySelector(".close").addEventListener("click", () => { + addFollowupModal.style.display = "none"; + addFollowupModalForm.reset(); + }); + addFollowupModal + .querySelector(".cancel-add-followup") + .addEventListener("click", () => { + addFollowupModal.style.display = "none"; + addFollowupModalForm.reset(); + }); + } + + // 取消编辑跟进弹窗 + if (editFollowupModal) { + editFollowupModal.querySelector(".close").addEventListener("click", () => { + editFollowupModal.style.display = "none"; + editFollowupForm.reset(); + }); + editFollowupModal + .querySelector(".cancel-edit-followup") + .addEventListener("click", () => { + editFollowupModal.style.display = "none"; + editFollowupForm.reset(); + }); + } + + // 旧表单相关逻辑保留兼容 + if (cancelFollowupBtn) { + cancelFollowupBtn.addEventListener("click", function () { + followupFormCard.style.display = "none"; + followupForm.reset(); + }); + } + + // Load customer list for follow-up dropdown from trial periods (for modal) + async function loadCustomerListForFollowupModal( + selectId = "addFollowupCustomerName", + ) { + try { + const response = await authenticatedFetch("/api/trial-customers/list"); + const data = await response.json(); + + const selectElement = document.getElementById(selectId); + if (!selectElement) return; + + selectElement.innerHTML = '请选择客户'; + if (data.customerNames && data.customerNames.length > 0) { + data.customerNames.forEach((customerName) => { + const option = document.createElement("option"); + option.value = customerName; + option.textContent = customerName; + selectElement.appendChild(option); }); + } + } catch (error) { + console.error("Error loading customer list:", error); } + } - // Load follow-ups - async function loadFollowUps() { - try { - const response = await authenticatedFetch(`/api/followups?page=${followupCurrentPage}&pageSize=${followupPageSize}`); - const data = await response.json(); + // Create follow-up + if (followupForm) { + followupForm.addEventListener("submit", async function (e) { + e.preventDefault(); - if (data.followUps) { - allFollowUps = data.followUps; - followupTotalItems = data.total || 0; - followupTotalPages = data.totalPages || 1; - renderFollowUpTable(allFollowUps); - updateFollowupPaginationControls(); - } - } catch (error) { - console.error('Error loading follow-ups:', error); + const followUpTimeValue = document.getElementById("followupTime").value; + const followUpTimeISO = new Date(followUpTimeValue).toISOString(); + + // Get customer name from the selected option's text + const customerSelect = document.getElementById("followupCustomerName"); + const selectedOption = + customerSelect.options[customerSelect.selectedIndex]; + const customerName = selectedOption ? selectedOption.textContent : ""; + + const formData = { + customerName: customerName, + dealStatus: document.getElementById("followupDealStatus").value, + customerLevel: document.getElementById("followupCustomerLevel").value, + industry: document.getElementById("followupIndustry").value, + followUpTime: followUpTimeISO, + }; + + try { + const response = await authenticatedFetch("/api/followups", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + if (response.ok) { + followupForm.reset(); + followupFormCard.style.display = "none"; + loadFollowUps(); + } else { + alert("创建跟进记录时出错"); } + } catch (error) { + console.error("Error creating follow-up:", error); + alert("创建跟进记录时出错"); + } + }); + } + + // Load follow-ups + async function loadFollowUps() { + try { + const response = await authenticatedFetch( + `/api/followups?page=${followupCurrentPage}&pageSize=${followupPageSize}`, + ); + const data = await response.json(); + + if (data.followUps) { + allFollowUps = data.followUps; + followupTotalItems = data.total || 0; + followupTotalPages = data.totalPages || 1; + renderFollowUpTable(allFollowUps); + updateFollowupPaginationControls(); + } + } catch (error) { + console.error("Error loading follow-ups:", error); } + } - // Render follow-up table - function renderFollowUpTable(followUps) { - followupTableBody.innerHTML = ''; + // Render follow-up table + function renderFollowUpTable(followUps) { + followupTableBody.innerHTML = ""; - followUps.forEach(followUp => { - const row = document.createElement('tr'); + followUps.forEach((followUp) => { + const row = document.createElement("tr"); - // Format follow-up time - const followUpTime = new Date(followUp.followUpTime); - const formattedTime = followUpTime.toLocaleString('zh-CN', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit' - }); + // Format follow-up time + const followUpTime = new Date(followUp.followUpTime); + const formattedTime = followUpTime.toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); - // Notification status - const notificationStatus = followUp.notificationSent ? - '已通知' : - '待通知'; + // Notification status + const notificationStatus = followUp.notificationSent + ? '已通知' + : '待通知'; - // Customer level display - let levelDisplay = followUp.customerLevel; - if (followUp.customerLevel === 'A') { - levelDisplay = 'A级 (重点客户)'; - } else if (followUp.customerLevel === 'B') { - levelDisplay = 'B级 (潜在客户)'; - } else if (followUp.customerLevel === 'C') { - levelDisplay = 'C级 (一般客户)'; - } + // Customer level display + let levelDisplay = followUp.customerLevel; + if (followUp.customerLevel === "A") { + levelDisplay = "A级 (重点客户)"; + } else if (followUp.customerLevel === "B") { + levelDisplay = "B级 (潜在客户)"; + } else if (followUp.customerLevel === "C") { + levelDisplay = "C级 (一般客户)"; + } - row.innerHTML = ` - ${followUp.customerName || ''} - ${followUp.dealStatus || ''} + row.innerHTML = ` + ${followUp.customerName || ""} + ${followUp.dealStatus || ""} ${levelDisplay} - ${followUp.industry || ''} + ${followUp.industry || ""} ${formattedTime} ${notificationStatus} - + `; - followupTableBody.appendChild(row); - }); + followupTableBody.appendChild(row); + }); - // Add edit event listeners - document.querySelectorAll('.edit-followup-btn').forEach(btn => { - btn.addEventListener('click', async function () { - const followUpId = this.getAttribute('data-id'); - await openEditFollowupModal(followUpId); - }); - }); + // Add edit event listeners + document.querySelectorAll(".edit-followup-btn").forEach((btn) => { + btn.addEventListener("click", async function () { + const followUpId = this.getAttribute("data-id"); + await openEditFollowupModal(followUpId); + }); + }); - // Add delete event listeners (only for users with delete permission) - document.querySelectorAll('.delete-followup-btn:not(.disabled)').forEach(btn => { - btn.addEventListener('click', function () { - const followUpId = this.getAttribute('data-id'); - deleteFollowUp(followUpId); - }); + // Add delete event listeners (only for users with delete permission) + document + .querySelectorAll(".delete-followup-btn:not(.disabled)") + .forEach((btn) => { + btn.addEventListener("click", function () { + const followUpId = this.getAttribute("data-id"); + deleteFollowUp(followUpId); }); + }); + } + + // Delete follow-up + async function deleteFollowUp(followUpId) { + if (!confirm("确定要删除这条跟进记录吗?")) { + return; } - // Delete follow-up - async function deleteFollowUp(followUpId) { - if (!confirm('确定要删除这条跟进记录吗?')) { - return; + try { + const response = await authenticatedFetch( + `/api/followups/${followUpId}`, + { + method: "DELETE", + }, + ); + + if (response.ok) { + loadFollowUps(); + } else { + alert("删除跟进记录时出错"); + } + } catch (error) { + console.error("Error deleting follow-up:", error); + alert("删除跟进记录时出错"); + } + } + + // 添加跟进弹窗表单提交 + if (addFollowupModalForm) { + addFollowupModalForm.addEventListener("submit", async function (e) { + e.preventDefault(); + + const followUpTimeValue = + document.getElementById("addFollowupTime").value; + const followUpTimeISO = new Date(followUpTimeValue).toISOString(); + + const customerSelect = document.getElementById("addFollowupCustomerName"); + const selectedOption = + customerSelect.options[customerSelect.selectedIndex]; + const customerName = selectedOption ? selectedOption.textContent : ""; + + const formData = { + customerName: customerName, + dealStatus: document.getElementById("addFollowupDealStatus").value, + customerLevel: document.getElementById("addFollowupCustomerLevel") + .value, + industry: document.getElementById("addFollowupIndustry").value, + followUpTime: followUpTimeISO, + }; + + try { + const response = await authenticatedFetch("/api/followups", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + if (response.ok) { + addFollowupModalForm.reset(); + addFollowupModal.style.display = "none"; + loadFollowUps(); + } else { + alert("创建跟进记录时出错"); } + } catch (error) { + console.error("Error creating follow-up:", error); + alert("创建跟进记录时出错"); + } + }); + } - try { - const response = await authenticatedFetch(`/api/followups/${followUpId}`, { - method: 'DELETE' - }); + // 打开编辑跟进弹窗 + async function openEditFollowupModal(followUpId) { + // 从缓存中查找跟进记录 + const followUp = allFollowUps.find((f) => f.id === followUpId); + if (!followUp) { + alert("未找到跟进记录"); + return; + } - if (response.ok) { - loadFollowUps(); - } else { - alert('删除跟进记录时出错'); - } - } catch (error) { - console.error('Error deleting follow-up:', error); - alert('删除跟进记录时出错'); + // 加载客户列表到编辑弹窗 + await loadCustomerListForFollowupModal("editFollowupCustomerName"); + + // 填充表单 + document.getElementById("editFollowupId").value = followUp.id; + document.getElementById("editFollowupCustomerName").value = + followUp.customerName || ""; + document.getElementById("editFollowupDealStatus").value = + followUp.dealStatus || ""; + document.getElementById("editFollowupCustomerLevel").value = + followUp.customerLevel || ""; + document.getElementById("editFollowupIndustry").value = + followUp.industry || ""; + + // 格式化时间为 datetime-local 格式 + if (followUp.followUpTime) { + const date = new Date(followUp.followUpTime); + const localDateTime = + date.getFullYear() + + "-" + + String(date.getMonth() + 1).padStart(2, "0") + + "-" + + String(date.getDate()).padStart(2, "0") + + "T" + + String(date.getHours()).padStart(2, "0") + + ":" + + String(date.getMinutes()).padStart(2, "0"); + document.getElementById("editFollowupTime").value = localDateTime; + } + + editFollowupModal.style.display = "block"; + } + + // 编辑跟进表单提交 + if (editFollowupForm) { + editFollowupForm.addEventListener("submit", async function (e) { + e.preventDefault(); + + const followUpId = document.getElementById("editFollowupId").value; + const followUpTimeValue = + document.getElementById("editFollowupTime").value; + const followUpTimeISO = new Date(followUpTimeValue).toISOString(); + + const customerSelect = document.getElementById( + "editFollowupCustomerName", + ); + const selectedOption = + customerSelect.options[customerSelect.selectedIndex]; + const customerName = selectedOption ? selectedOption.textContent : ""; + + const formData = { + customerName: customerName, + dealStatus: document.getElementById("editFollowupDealStatus").value, + customerLevel: document.getElementById("editFollowupCustomerLevel") + .value, + industry: document.getElementById("editFollowupIndustry").value, + followUpTime: followUpTimeISO, + }; + + try { + const response = await authenticatedFetch( + `/api/followups/${followUpId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }, + ); + + if (response.ok) { + editFollowupForm.reset(); + editFollowupModal.style.display = "none"; + loadFollowUps(); + } else { + alert("更新跟进记录时出错"); } + } catch (error) { + console.error("Error updating follow-up:", error); + alert("更新跟进记录时出错"); + } + }); + } + + // Update follow-up pagination controls + function updateFollowupPaginationControls() { + const startItem = + followupTotalItems === 0 + ? 0 + : (followupCurrentPage - 1) * followupPageSize + 1; + const endItem = Math.min( + followupCurrentPage * followupPageSize, + followupTotalItems, + ); + + document.getElementById("followupPaginationInfo").textContent = + `显示 ${startItem}-${endItem} 共 ${followupTotalItems} 条`; + + document.getElementById("followupFirstPage").disabled = + followupCurrentPage === 1; + document.getElementById("followupPrevPage").disabled = + followupCurrentPage === 1; + document.getElementById("followupNextPage").disabled = + followupCurrentPage === followupTotalPages; + document.getElementById("followupLastPage").disabled = + followupCurrentPage === followupTotalPages; + + const pageNumbers = document.getElementById("followupPageNumbers"); + pageNumbers.innerHTML = ""; + + const maxVisiblePages = 5; + let startPage = Math.max( + 1, + followupCurrentPage - Math.floor(maxVisiblePages / 2), + ); + let endPage = Math.min(followupTotalPages, startPage + maxVisiblePages - 1); + + if (endPage - startPage + 1 < maxVisiblePages) { + startPage = Math.max(1, endPage - maxVisiblePages + 1); } - // 添加跟进弹窗表单提交 - if (addFollowupModalForm) { - addFollowupModalForm.addEventListener('submit', async function (e) { - e.preventDefault(); - - const followUpTimeValue = document.getElementById('addFollowupTime').value; - const followUpTimeISO = new Date(followUpTimeValue).toISOString(); - - const customerSelect = document.getElementById('addFollowupCustomerName'); - const selectedOption = customerSelect.options[customerSelect.selectedIndex]; - const customerName = selectedOption ? selectedOption.textContent : ''; - - const formData = { - customerName: customerName, - dealStatus: document.getElementById('addFollowupDealStatus').value, - customerLevel: document.getElementById('addFollowupCustomerLevel').value, - industry: document.getElementById('addFollowupIndustry').value, - followUpTime: followUpTimeISO - }; - - try { - const response = await authenticatedFetch('/api/followups', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(formData) - }); - - if (response.ok) { - addFollowupModalForm.reset(); - addFollowupModal.style.display = 'none'; - loadFollowUps(); - } else { - alert('创建跟进记录时出错'); - } - } catch (error) { - console.error('Error creating follow-up:', error); - alert('创建跟进记录时出错'); - } - }); + for (let i = startPage; i <= endPage; i++) { + const pageBtn = document.createElement("button"); + pageBtn.className = `page-number ${i === followupCurrentPage ? "active" : ""}`; + pageBtn.textContent = i; + pageBtn.addEventListener("click", () => { + followupCurrentPage = i; + loadFollowUps(); + }); + pageNumbers.appendChild(pageBtn); } + } - // 打开编辑跟进弹窗 - async function openEditFollowupModal(followUpId) { - // 从缓存中查找跟进记录 - const followUp = allFollowUps.find(f => f.id === followUpId); - if (!followUp) { - alert('未找到跟进记录'); - return; + // Follow-up pagination event listeners + if (document.getElementById("followupFirstPage")) { + document + .getElementById("followupFirstPage") + .addEventListener("click", () => { + if (followupCurrentPage > 1) { + followupCurrentPage = 1; + loadFollowUps(); } + }); + } - // 加载客户列表到编辑弹窗 - await loadCustomerListForFollowupModal('editFollowupCustomerName'); - - // 填充表单 - document.getElementById('editFollowupId').value = followUp.id; - document.getElementById('editFollowupCustomerName').value = followUp.customerName || ''; - document.getElementById('editFollowupDealStatus').value = followUp.dealStatus || ''; - document.getElementById('editFollowupCustomerLevel').value = followUp.customerLevel || ''; - document.getElementById('editFollowupIndustry').value = followUp.industry || ''; - - // 格式化时间为 datetime-local 格式 - if (followUp.followUpTime) { - const date = new Date(followUp.followUpTime); - const localDateTime = date.getFullYear() + '-' + - String(date.getMonth() + 1).padStart(2, '0') + '-' + - String(date.getDate()).padStart(2, '0') + 'T' + - String(date.getHours()).padStart(2, '0') + ':' + - String(date.getMinutes()).padStart(2, '0'); - document.getElementById('editFollowupTime').value = localDateTime; + if (document.getElementById("followupPrevPage")) { + document + .getElementById("followupPrevPage") + .addEventListener("click", () => { + if (followupCurrentPage > 1) { + followupCurrentPage--; + loadFollowUps(); } + }); + } - editFollowupModal.style.display = 'block'; - } - - // 编辑跟进表单提交 - if (editFollowupForm) { - editFollowupForm.addEventListener('submit', async function (e) { - e.preventDefault(); - - const followUpId = document.getElementById('editFollowupId').value; - const followUpTimeValue = document.getElementById('editFollowupTime').value; - const followUpTimeISO = new Date(followUpTimeValue).toISOString(); - - const customerSelect = document.getElementById('editFollowupCustomerName'); - const selectedOption = customerSelect.options[customerSelect.selectedIndex]; - const customerName = selectedOption ? selectedOption.textContent : ''; - - const formData = { - customerName: customerName, - dealStatus: document.getElementById('editFollowupDealStatus').value, - customerLevel: document.getElementById('editFollowupCustomerLevel').value, - industry: document.getElementById('editFollowupIndustry').value, - followUpTime: followUpTimeISO - }; - - try { - const response = await authenticatedFetch(`/api/followups/${followUpId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(formData) - }); - - if (response.ok) { - editFollowupForm.reset(); - editFollowupModal.style.display = 'none'; - loadFollowUps(); - } else { - alert('更新跟进记录时出错'); - } - } catch (error) { - console.error('Error updating follow-up:', error); - alert('更新跟进记录时出错'); - } - }); - } - - // Update follow-up pagination controls - function updateFollowupPaginationControls() { - const startItem = followupTotalItems === 0 ? 0 : (followupCurrentPage - 1) * followupPageSize + 1; - const endItem = Math.min(followupCurrentPage * followupPageSize, followupTotalItems); - - document.getElementById('followupPaginationInfo').textContent = - `显示 ${startItem}-${endItem} 共 ${followupTotalItems} 条`; - - document.getElementById('followupFirstPage').disabled = followupCurrentPage === 1; - document.getElementById('followupPrevPage').disabled = followupCurrentPage === 1; - document.getElementById('followupNextPage').disabled = followupCurrentPage === followupTotalPages; - document.getElementById('followupLastPage').disabled = followupCurrentPage === followupTotalPages; - - const pageNumbers = document.getElementById('followupPageNumbers'); - pageNumbers.innerHTML = ''; - - const maxVisiblePages = 5; - let startPage = Math.max(1, followupCurrentPage - Math.floor(maxVisiblePages / 2)); - let endPage = Math.min(followupTotalPages, startPage + maxVisiblePages - 1); - - if (endPage - startPage + 1 < maxVisiblePages) { - startPage = Math.max(1, endPage - maxVisiblePages + 1); + if (document.getElementById("followupNextPage")) { + document + .getElementById("followupNextPage") + .addEventListener("click", () => { + if (followupCurrentPage < followupTotalPages) { + followupCurrentPage++; + loadFollowUps(); } + }); + } - for (let i = startPage; i <= endPage; i++) { - const pageBtn = document.createElement('button'); - pageBtn.className = `page-number ${i === followupCurrentPage ? 'active' : ''}`; - pageBtn.textContent = i; - pageBtn.addEventListener('click', () => { - followupCurrentPage = i; - loadFollowUps(); - }); - pageNumbers.appendChild(pageBtn); + if (document.getElementById("followupLastPage")) { + document + .getElementById("followupLastPage") + .addEventListener("click", () => { + if (followupCurrentPage < followupTotalPages) { + followupCurrentPage = followupTotalPages; + loadFollowUps(); } - } + }); + } - // Follow-up pagination event listeners - if (document.getElementById('followupFirstPage')) { - document.getElementById('followupFirstPage').addEventListener('click', () => { - if (followupCurrentPage > 1) { - followupCurrentPage = 1; - loadFollowUps(); - } - }); - } + if (document.getElementById("followupPageSizeSelect")) { + document + .getElementById("followupPageSizeSelect") + .addEventListener("change", (e) => { + followupPageSize = parseInt(e.target.value); + followupCurrentPage = 1; + loadFollowUps(); + }); + } - if (document.getElementById('followupPrevPage')) { - document.getElementById('followupPrevPage').addEventListener('click', () => { - if (followupCurrentPage > 1) { - followupCurrentPage--; - loadFollowUps(); - } - }); - } - - if (document.getElementById('followupNextPage')) { - document.getElementById('followupNextPage').addEventListener('click', () => { - if (followupCurrentPage < followupTotalPages) { - followupCurrentPage++; - loadFollowUps(); - } - }); - } - - if (document.getElementById('followupLastPage')) { - document.getElementById('followupLastPage').addEventListener('click', () => { - if (followupCurrentPage < followupTotalPages) { - followupCurrentPage = followupTotalPages; - loadFollowUps(); - } - }); - } - - if (document.getElementById('followupPageSizeSelect')) { - document.getElementById('followupPageSizeSelect').addEventListener('change', (e) => { - followupPageSize = parseInt(e.target.value); - followupCurrentPage = 1; - loadFollowUps(); - }); - } - - // Refresh follow-ups button - if (refreshFollowupsBtn) { - refreshFollowupsBtn.addEventListener('click', async () => { - // Add refreshing animation - refreshFollowupsBtn.classList.add('refreshing'); - const table = document.getElementById('followupTable'); - try { - await loadFollowUps(); - // Show success feedback briefly - refreshFollowupsBtn.classList.remove('refreshing'); - refreshFollowupsBtn.classList.add('refresh-success'); - // Add table refresh animation - if (table) { - table.classList.add('table-refreshing'); - setTimeout(() => { - table.classList.remove('table-refreshing'); - }, 500); - } - setTimeout(() => { - refreshFollowupsBtn.classList.remove('refresh-success'); - }, 1000); - } catch (error) { - refreshFollowupsBtn.classList.remove('refreshing'); - } - }); - } - - // ========================================== - // Global Search Functionality - // ========================================== - const globalSearchInput = document.getElementById('globalSearchInput'); - const searchResultsDropdown = document.getElementById('searchResultsDropdown'); - const searchResultsItems = document.getElementById('searchResultsItems'); - const searchHistoryItems = document.getElementById('searchHistoryItems'); - - let searchHistory = JSON.parse(localStorage.getItem('crmSearchHistory') || '[]'); - let searchDebounceTimer = null; - - // Render search history - function renderSearchHistory() { - if (searchHistory.length === 0) { - searchHistoryItems.innerHTML = '暂无搜索历史'; - return; + // Refresh follow-ups button + if (refreshFollowupsBtn) { + refreshFollowupsBtn.addEventListener("click", async () => { + // Add refreshing animation + refreshFollowupsBtn.classList.add("refreshing"); + const table = document.getElementById("followupTable"); + try { + await loadFollowUps(); + // Show success feedback briefly + refreshFollowupsBtn.classList.remove("refreshing"); + refreshFollowupsBtn.classList.add("refresh-success"); + // Add table refresh animation + if (table) { + table.classList.add("table-refreshing"); + setTimeout(() => { + table.classList.remove("table-refreshing"); + }, 500); } - searchHistoryItems.innerHTML = searchHistory.slice(0, 5).map(item => ` + setTimeout(() => { + refreshFollowupsBtn.classList.remove("refresh-success"); + }, 1000); + } catch (error) { + refreshFollowupsBtn.classList.remove("refreshing"); + } + }); + } + + // ========================================== + // Global Search Functionality + // ========================================== + const globalSearchInput = document.getElementById("globalSearchInput"); + const searchResultsDropdown = document.getElementById( + "searchResultsDropdown", + ); + const searchResultsItems = document.getElementById("searchResultsItems"); + const searchHistoryItems = document.getElementById("searchHistoryItems"); + + let searchHistory = JSON.parse( + localStorage.getItem("crmSearchHistory") || "[]", + ); + let searchDebounceTimer = null; + + // Render search history + function renderSearchHistory() { + if (searchHistory.length === 0) { + searchHistoryItems.innerHTML = + '暂无搜索历史'; + return; + } + searchHistoryItems.innerHTML = searchHistory + .slice(0, 5) + .map( + (item) => ` ${item} - `).join(''); + `, + ) + .join(""); - // Add click handlers for history items - searchHistoryItems.querySelectorAll('.search-history-item').forEach(el => { - el.addEventListener('click', () => { - globalSearchInput.value = el.dataset.query; - performGlobalSearch(el.dataset.query); - }); + // Add click handlers for history items + searchHistoryItems + .querySelectorAll(".search-history-item") + .forEach((el) => { + el.addEventListener("click", () => { + globalSearchInput.value = el.dataset.query; + performGlobalSearch(el.dataset.query); }); + }); + } + + // Save to search history + function saveToSearchHistory(query) { + if (!query.trim()) return; + searchHistory = [query, ...searchHistory.filter((h) => h !== query)].slice( + 0, + 10, + ); + localStorage.setItem("crmSearchHistory", JSON.stringify(searchHistory)); + renderSearchHistory(); + } + + // Perform global search + async function performGlobalSearch(query) { + if (!query.trim()) { + searchResultsItems.innerHTML = + '输入关键词开始搜索...'; + return; } - // Save to search history - function saveToSearchHistory(query) { - if (!query.trim()) return; - searchHistory = [query, ...searchHistory.filter(h => h !== query)].slice(0, 10); - localStorage.setItem('crmSearchHistory', JSON.stringify(searchHistory)); - renderSearchHistory(); - } + searchResultsItems.innerHTML = + ' 搜索中...'; - // Perform global search - async function performGlobalSearch(query) { - if (!query.trim()) { - searchResultsItems.innerHTML = '输入关键词开始搜索...'; - return; + try { + const results = []; + + // Search in trial periods + try { + const trialResponse = await authenticatedFetch( + "/api/trial-periods/all", + ); + if (trialResponse.ok) { + const trialData = await trialResponse.json(); + const trialPeriods = trialData.trialPeriods || []; + + // Get customers map + const customersResponse = await authenticatedFetch( + "/api/customers/list", + ); + const customersData = customersResponse.ok + ? await customersResponse.json() + : {}; + const customersMap = customersData.customerMap || {}; + + // Search trial periods + trialPeriods.forEach((period) => { + const customerName = + customersMap[period.customerId] || period.customerId || ""; + if ( + customerName && + customerName.toLowerCase().includes(query.toLowerCase()) + ) { + results.push({ + type: "trial", + title: customerName, + meta: `试用时间: ${period.startTime ? period.startTime.split("T")[0] : ""} ~ ${period.endTime ? period.endTime.split("T")[0] : ""}`, + icon: "fas fa-clock", + section: "trialPeriods", + }); + } + }); } + } catch (trialError) { + console.warn("Trial periods search error:", trialError); + } - searchResultsItems.innerHTML = ' 搜索中...'; - - try { - const results = []; - - // Search in trial periods - try { - const trialResponse = await authenticatedFetch('/api/trial-periods/all'); - if (trialResponse.ok) { - const trialData = await trialResponse.json(); - const trialPeriods = trialData.trialPeriods || []; - - // Get customers map - const customersResponse = await authenticatedFetch('/api/customers/list'); - const customersData = customersResponse.ok ? await customersResponse.json() : {}; - const customersMap = customersData.customerMap || {}; - - // Search trial periods - trialPeriods.forEach(period => { - const customerName = customersMap[period.customerId] || period.customerId || ''; - if (customerName && customerName.toLowerCase().includes(query.toLowerCase())) { - results.push({ - type: 'trial', - title: customerName, - meta: `试用时间: ${period.startTime ? period.startTime.split('T')[0] : ''} ~ ${period.endTime ? period.endTime.split('T')[0] : ''}`, - icon: 'fas fa-clock', - section: 'trialPeriods' - }); - } - }); - } - } catch (trialError) { - console.warn('Trial periods search error:', trialError); + // Search in customers (weekly progress) - use local allCustomers if available + try { + if (typeof allCustomers !== "undefined" && allCustomers.length > 0) { + allCustomers.forEach((customer) => { + const searchFields = [ + customer.customerName || "", + customer.description || "", + customer.solution || "", + customer.module || "", + ] + .join(" ") + .toLowerCase(); + if (searchFields.includes(query.toLowerCase())) { + results.push({ + type: "progress", + title: customer.customerName || "未知客户", + meta: customer.description + ? customer.description.substring(0, 50) + "..." + : "", + icon: "fas fa-user-plus", + section: "customer", + }); } + }); + } else { + const progressResponse = await authenticatedFetch( + "/api/customers?page=1&pageSize=100", + ); + if (progressResponse.ok) { + const progressData = await progressResponse.json(); + const customers = progressData.customers || []; - // Search in customers (weekly progress) - use local allCustomers if available - try { - if (typeof allCustomers !== 'undefined' && allCustomers.length > 0) { - allCustomers.forEach(customer => { - const searchFields = [ - customer.customerName || '', - customer.description || '', - customer.solution || '', - customer.module || '' - ].join(' ').toLowerCase(); - if (searchFields.includes(query.toLowerCase())) { - results.push({ - type: 'progress', - title: customer.customerName || '未知客户', - meta: customer.description ? customer.description.substring(0, 50) + '...' : '', - icon: 'fas fa-user-plus', - section: 'customer' - }); - } - }); - } else { - const progressResponse = await authenticatedFetch('/api/customers?page=1&pageSize=100'); - if (progressResponse.ok) { - const progressData = await progressResponse.json(); - const customers = progressData.customers || []; - - customers.forEach(customer => { - const searchFields = [ - customer.customerName || '', - customer.description || '', - customer.solution || '', - customer.module || '' - ].join(' ').toLowerCase(); - if (searchFields.includes(query.toLowerCase())) { - results.push({ - type: 'progress', - title: customer.customerName || '未知客户', - meta: customer.description ? customer.description.substring(0, 50) + '...' : '', - icon: 'fas fa-user-plus', - section: 'customer' - }); - } - }); - } - } - } catch (progressError) { - console.warn('Progress search error:', progressError); - } - - // Search in followups - try { - const followupResponse = await authenticatedFetch('/api/followups'); - if (followupResponse.ok) { - const followupData = await followupResponse.json(); - const followups = followupData.followups || []; - - followups.forEach(followup => { - if (followup.customerName && followup.customerName.toLowerCase().includes(query.toLowerCase())) { - results.push({ - type: 'followup', - title: followup.customerName, - meta: `状态: ${followup.dealStatus || '未知'} | 级别: ${followup.customerLevel || '未知'}`, - icon: 'fas fa-tasks', - section: 'followup' - }); - } - }); - } - } catch (followupError) { - console.warn('Followup search error:', followupError); - } - - // Render results - if (results.length === 0) { - searchResultsItems.innerHTML = '未找到匹配结果'; - } else { - // Deduplicate by title - const seen = new Set(); - const uniqueResults = results.filter(r => { - const key = r.title + r.type; - if (seen.has(key)) return false; - seen.add(key); - return true; + customers.forEach((customer) => { + const searchFields = [ + customer.customerName || "", + customer.description || "", + customer.solution || "", + customer.module || "", + ] + .join(" ") + .toLowerCase(); + if (searchFields.includes(query.toLowerCase())) { + results.push({ + type: "progress", + title: customer.customerName || "未知客户", + meta: customer.description + ? customer.description.substring(0, 50) + "..." + : "", + icon: "fas fa-user-plus", + section: "customer", }); + } + }); + } + } + } catch (progressError) { + console.warn("Progress search error:", progressError); + } - searchResultsItems.innerHTML = uniqueResults.slice(0, 10).map(result => ` + // Search in followups + try { + const followupResponse = await authenticatedFetch("/api/followups"); + if (followupResponse.ok) { + const followupData = await followupResponse.json(); + const followups = followupData.followups || []; + + followups.forEach((followup) => { + if ( + followup.customerName && + followup.customerName.toLowerCase().includes(query.toLowerCase()) + ) { + results.push({ + type: "followup", + title: followup.customerName, + meta: `状态: ${followup.dealStatus || "未知"} | 级别: ${followup.customerLevel || "未知"}`, + icon: "fas fa-tasks", + section: "followup", + }); + } + }); + } + } catch (followupError) { + console.warn("Followup search error:", followupError); + } + + // Render results + if (results.length === 0) { + searchResultsItems.innerHTML = + '未找到匹配结果'; + } else { + // Deduplicate by title + const seen = new Set(); + const uniqueResults = results.filter((r) => { + const key = r.title + r.type; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + searchResultsItems.innerHTML = uniqueResults + .slice(0, 10) + .map( + (result) => ` ${result.title} ${result.meta} - ${result.type === 'trial' ? '试用' : result.type === 'progress' ? '进度' : '跟进'} + ${result.type === "trial" ? "试用" : result.type === "progress" ? "进度" : "跟进"} - `).join(''); + `, + ) + .join(""); - // Add click handlers - searchResultsItems.querySelectorAll('.search-result-item').forEach(el => { - el.addEventListener('click', () => { - saveToSearchHistory(query); - switchSection(el.dataset.section); - searchResultsDropdown.style.display = 'none'; - globalSearchInput.value = ''; - }); - }); - } - } catch (error) { - console.error('Search error:', error); - searchResultsItems.innerHTML = '搜索出错,请重试'; - } - } - - // Search input event handlers - if (globalSearchInput) { - globalSearchInput.addEventListener('focus', () => { - renderSearchHistory(); - searchResultsDropdown.style.display = 'block'; - }); - - globalSearchInput.addEventListener('input', (e) => { - clearTimeout(searchDebounceTimer); - searchDebounceTimer = setTimeout(() => { - performGlobalSearch(e.target.value); - }, 300); - }); - - globalSearchInput.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - searchResultsDropdown.style.display = 'none'; - globalSearchInput.blur(); - } - if (e.key === 'Enter') { - saveToSearchHistory(globalSearchInput.value); - } - }); - - // Close dropdown when clicking outside - document.addEventListener('click', (e) => { - if (!e.target.closest('.global-search-container')) { - searchResultsDropdown.style.display = 'none'; - } - }); - - // Keyboard shortcut (Cmd+K / Ctrl+K) - document.addEventListener('keydown', (e) => { - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { - e.preventDefault(); - globalSearchInput.focus(); - } - }); - } - - // ========================================== - // Header Quick Add Button - // ========================================== - const headerQuickAddBtn = document.getElementById('headerQuickAddBtn'); - const headerQuickAddDropdown = document.getElementById('headerQuickAddDropdown'); - - if (headerQuickAddBtn) { - headerQuickAddBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const isVisible = headerQuickAddDropdown.style.display === 'block'; - headerQuickAddDropdown.style.display = isVisible ? 'none' : 'block'; - }); - - // Quick add menu item handlers - document.querySelectorAll('.quick-add-menu-item').forEach(item => { - item.addEventListener('click', () => { - const action = item.dataset.action; - headerQuickAddDropdown.style.display = 'none'; - - if (action === 'trial') { - switchSection('trialPeriods'); - setTimeout(() => { - const addTrialBtn = document.getElementById('addTrialBtn'); - if (addTrialBtn) addTrialBtn.click(); - }, 100); - } else if (action === 'customer') { - switchSection('customer'); - setTimeout(() => { - const addCustomerBtn = document.getElementById('addCustomerBtn'); - if (addCustomerBtn) addCustomerBtn.click(); - }, 100); - } else if (action === 'followup') { - switchSection('followup'); - setTimeout(() => { - const addFollowUpBtn = document.getElementById('addFollowUpBtn'); - if (addFollowUpBtn) addFollowUpBtn.click(); - }, 100); - } + // Add click handlers + searchResultsItems + .querySelectorAll(".search-result-item") + .forEach((el) => { + el.addEventListener("click", () => { + saveToSearchHistory(query); + switchSection(el.dataset.section); + searchResultsDropdown.style.display = "none"; + globalSearchInput.value = ""; }); - }); - - // Close dropdown when clicking outside - document.addEventListener('click', (e) => { - if (!e.target.closest('.quick-add-dropdown-container')) { - headerQuickAddDropdown.style.display = 'none'; - } - }); + }); + } + } catch (error) { + console.error("Search error:", error); + searchResultsItems.innerHTML = + '搜索出错,请重试'; } + } - // ========================================== - // Logout Button - // ========================================== - const headerLogoutBtn = document.getElementById('logoutBtn'); - if (headerLogoutBtn) { - headerLogoutBtn.addEventListener('click', () => { - if (confirm('确定要退出登录吗?')) { - localStorage.removeItem('crmToken'); - localStorage.removeItem('crmSearchHistory'); - window.location.href = '/static/login.html'; - } - }); - } + // Search input event handlers + if (globalSearchInput) { + globalSearchInput.addEventListener("focus", () => { + renderSearchHistory(); + searchResultsDropdown.style.display = "block"; + }); - // ========================================== - // Customer Progress Tabs - // ========================================== - const progressListTab = document.getElementById('progressListTab'); - const weeklySummaryTab = document.getElementById('weeklySummaryTab'); - const progressListView = document.getElementById('progressListView'); - const weeklySummaryView = document.getElementById('weeklySummaryView'); + globalSearchInput.addEventListener("input", (e) => { + clearTimeout(searchDebounceTimer); + searchDebounceTimer = setTimeout(() => { + performGlobalSearch(e.target.value); + }, 300); + }); - // Week offset for navigation (0 = current week, -1 = last week, etc.) - let weekOffset = 0; + globalSearchInput.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + searchResultsDropdown.style.display = "none"; + globalSearchInput.blur(); + } + if (e.key === "Enter") { + saveToSearchHistory(globalSearchInput.value); + } + }); - // Tab switching - function switchCustomerTab(tabName) { - // Update tab buttons - document.querySelectorAll('.customer-tab').forEach(tab => { - tab.classList.remove('active'); - }); + // Close dropdown when clicking outside + document.addEventListener("click", (e) => { + if (!e.target.closest(".global-search-container")) { + searchResultsDropdown.style.display = "none"; + } + }); - // Update tab content - document.querySelectorAll('.tab-content').forEach(content => { - content.classList.remove('active'); - }); + // Keyboard shortcut (Cmd+K / Ctrl+K) + document.addEventListener("keydown", (e) => { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + globalSearchInput.focus(); + } + }); + } - // Get the table actions (refresh & export buttons) - const tableActions = document.querySelector('#customerSection .card-header .table-actions'); + // ========================================== + // Header Quick Add Button + // ========================================== + const headerQuickAddBtn = document.getElementById("headerQuickAddBtn"); + const headerQuickAddDropdown = document.getElementById( + "headerQuickAddDropdown", + ); - if (tabName === 'progress-list') { - progressListTab?.classList.add('active'); - progressListView?.classList.add('active'); - // Show refresh and export buttons for progress list - if (tableActions) tableActions.style.display = 'flex'; - } else if (tabName === 'weekly-summary') { - weeklySummaryTab?.classList.add('active'); - weeklySummaryView?.classList.add('active'); - // Hide refresh and export buttons for weekly summary - if (tableActions) tableActions.style.display = 'none'; - // Load weekly summary when switching to this tab - weekOffset = 0; - loadWeeklySummary(); + if (headerQuickAddBtn) { + headerQuickAddBtn.addEventListener("click", (e) => { + e.stopPropagation(); + const isVisible = headerQuickAddDropdown.style.display === "block"; + headerQuickAddDropdown.style.display = isVisible ? "none" : "block"; + }); + + // Quick add menu item handlers + document.querySelectorAll(".quick-add-menu-item").forEach((item) => { + item.addEventListener("click", () => { + const action = item.dataset.action; + headerQuickAddDropdown.style.display = "none"; + + if (action === "trial") { + switchSection("trialPeriods"); + setTimeout(() => { + const addTrialBtn = document.getElementById("addTrialBtn"); + if (addTrialBtn) addTrialBtn.click(); + }, 100); + } else if (action === "customer") { + switchSection("customer"); + setTimeout(() => { + const addCustomerBtn = document.getElementById("addCustomerBtn"); + if (addCustomerBtn) addCustomerBtn.click(); + }, 100); + } else if (action === "followup") { + switchSection("followup"); + setTimeout(() => { + const addFollowUpBtn = document.getElementById("addFollowUpBtn"); + if (addFollowUpBtn) addFollowUpBtn.click(); + }, 100); } - } - - // Tab click handlers - progressListTab?.addEventListener('click', () => switchCustomerTab('progress-list')); - weeklySummaryTab?.addEventListener('click', () => switchCustomerTab('weekly-summary')); - - // Week navigation handlers - document.getElementById('prevWeekBtn')?.addEventListener('click', () => { - weekOffset--; - loadWeeklySummary(); + }); }); - document.getElementById('nextWeekBtn')?.addEventListener('click', () => { - weekOffset++; - loadWeeklySummary(); + // Close dropdown when clicking outside + document.addEventListener("click", (e) => { + if (!e.target.closest(".quick-add-dropdown-container")) { + headerQuickAddDropdown.style.display = "none"; + } + }); + } + + // ========================================== + // Logout Button + // ========================================== + const headerLogoutBtn = document.getElementById("logoutBtn"); + if (headerLogoutBtn) { + headerLogoutBtn.addEventListener("click", () => { + if (confirm("确定要退出登录吗?")) { + localStorage.removeItem("crmToken"); + localStorage.removeItem("crmSearchHistory"); + window.location.href = "/static/login.html"; + } + }); + } + + // ========================================== + // Customer Progress Tabs + // ========================================== + const progressListTab = document.getElementById("progressListTab"); + const weeklySummaryTab = document.getElementById("weeklySummaryTab"); + const progressListView = document.getElementById("progressListView"); + const weeklySummaryView = document.getElementById("weeklySummaryView"); + + // Week offset for navigation (0 = current week, -1 = last week, etc.) + let weekOffset = 0; + + // Tab switching + function switchCustomerTab(tabName) { + // Update tab buttons + document.querySelectorAll(".customer-tab").forEach((tab) => { + tab.classList.remove("active"); }); - document.getElementById('currentWeekBtn')?.addEventListener('click', () => { - weekOffset = 0; - loadWeeklySummary(); + // Update tab content + document.querySelectorAll(".tab-content").forEach((content) => { + content.classList.remove("active"); }); - // Get week date range - function getWeekRange(offset = 0) { - const now = new Date(); - const currentDay = now.getDay(); - const diff = currentDay === 0 ? 6 : currentDay - 1; // Adjust for Monday start + // Get the table actions (refresh & export buttons) + const tableActions = document.querySelector( + "#customerSection .card-header .table-actions", + ); - const monday = new Date(now); - monday.setDate(now.getDate() - diff + (offset * 7)); - monday.setHours(0, 0, 0, 0); + if (tabName === "progress-list") { + progressListTab?.classList.add("active"); + progressListView?.classList.add("active"); + // Show refresh and export buttons for progress list + if (tableActions) tableActions.style.display = "flex"; + } else if (tabName === "weekly-summary") { + weeklySummaryTab?.classList.add("active"); + weeklySummaryView?.classList.add("active"); + // Hide refresh and export buttons for weekly summary + if (tableActions) tableActions.style.display = "none"; + // Load weekly summary when switching to this tab + weekOffset = 0; + loadWeeklySummary(); + } + } - const sunday = new Date(monday); - sunday.setDate(monday.getDate() + 6); - sunday.setHours(23, 59, 59, 999); + // Tab click handlers + progressListTab?.addEventListener("click", () => + switchCustomerTab("progress-list"), + ); + weeklySummaryTab?.addEventListener("click", () => + switchCustomerTab("weekly-summary"), + ); - return { start: monday, end: sunday }; + // Week navigation handlers + document.getElementById("prevWeekBtn")?.addEventListener("click", () => { + weekOffset--; + loadWeeklySummary(); + }); + + document.getElementById("nextWeekBtn")?.addEventListener("click", () => { + weekOffset++; + loadWeeklySummary(); + }); + + document.getElementById("currentWeekBtn")?.addEventListener("click", () => { + weekOffset = 0; + loadWeeklySummary(); + }); + + // Get week date range + function getWeekRange(offset = 0) { + const now = new Date(); + const currentDay = now.getDay(); + const diff = currentDay === 0 ? 6 : currentDay - 1; // Adjust for Monday start + + const monday = new Date(now); + monday.setDate(now.getDate() - diff + offset * 7); + monday.setHours(0, 0, 0, 0); + + const sunday = new Date(monday); + sunday.setDate(monday.getDate() + 6); + sunday.setHours(23, 59, 59, 999); + + return { start: monday, end: sunday }; + } + + // Format date for display + function formatWeekDate(date) { + const month = date.getMonth() + 1; + const day = date.getDate(); + return `${month}月${day}日`; + } + + // Format date to YYYY-MM-DD for comparison + function formatDateForCompare(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + } + + // Parse date string from customer data + function parseCustomerDate(dateStr) { + if (!dateStr) return null; + const cleaned = String(dateStr) + .trim() + .replace(/\./g, "-") + .replace(/\//g, "-"); + const parts = cleaned.split("-").filter(Boolean); + if (parts.length < 3) return null; + return new Date( + parseInt(parts[0]), + parseInt(parts[1]) - 1, + parseInt(parts[2]), + ); + } + + // Load and render weekly summary + function loadWeeklySummary() { + const weekRange = getWeekRange(weekOffset); + + // Update week range display + const weekRangeEl = document.getElementById("currentWeekRange"); + if (weekRangeEl) { + const weekLabel = + weekOffset === 0 + ? "本周" + : weekOffset === -1 + ? "上周" + : `${Math.abs(weekOffset)}周${weekOffset < 0 ? "前" : "后"}`; + weekRangeEl.textContent = `${weekLabel}: ${formatWeekDate(weekRange.start)} - ${formatWeekDate(weekRange.end)}`; } - // Format date for display - function formatWeekDate(date) { - const month = date.getMonth() + 1; - const day = date.getDate(); - return `${month}月${day}日`; - } + // Filter customers by week + const weekStart = formatDateForCompare(weekRange.start); + const weekEnd = formatDateForCompare(weekRange.end); - // Format date to YYYY-MM-DD for comparison - function formatDateForCompare(date) { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; - } + const weeklyCustomers = allCustomers.filter((customer) => { + const customerDate = parseCustomerDate(customer.intendedProduct); + if (!customerDate) return false; + const dateStr = formatDateForCompare(customerDate); + return dateStr >= weekStart && dateStr <= weekEnd; + }); - // Parse date string from customer data - function parseCustomerDate(dateStr) { - if (!dateStr) return null; - const cleaned = String(dateStr).trim().replace(/\./g, '-').replace(/\//g, '-'); - const parts = cleaned.split('-').filter(Boolean); - if (parts.length < 3) return null; - return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); - } + // Group by customer name + const customerGroups = {}; + weeklyCustomers.forEach((customer) => { + const name = customer.customerName || "未知客户"; + if (!customerGroups[name]) { + customerGroups[name] = []; + } + customerGroups[name].push(customer); + }); - // Load and render weekly summary - function loadWeeklySummary() { - const weekRange = getWeekRange(weekOffset); + // Render weekly summary + renderWeeklySummary(customerGroups); + } - // Update week range display - const weekRangeEl = document.getElementById('currentWeekRange'); - if (weekRangeEl) { - const weekLabel = weekOffset === 0 ? '本周' : (weekOffset === -1 ? '上周' : `${Math.abs(weekOffset)}周${weekOffset < 0 ? '前' : '后'}`); - weekRangeEl.textContent = `${weekLabel}: ${formatWeekDate(weekRange.start)} - ${formatWeekDate(weekRange.end)}`; - } + // Render weekly summary cards + function renderWeeklySummary(customerGroups) { + const container = document.getElementById("weeklySummaryContainer"); + if (!container) return; - // Filter customers by week - const weekStart = formatDateForCompare(weekRange.start); - const weekEnd = formatDateForCompare(weekRange.end); + container.innerHTML = ""; - const weeklyCustomers = allCustomers.filter(customer => { - const customerDate = parseCustomerDate(customer.intendedProduct); - if (!customerDate) return false; - const dateStr = formatDateForCompare(customerDate); - return dateStr >= weekStart && dateStr <= weekEnd; - }); + const customerNames = Object.keys(customerGroups); - // Group by customer name - const customerGroups = {}; - weeklyCustomers.forEach(customer => { - const name = customer.customerName || '未知客户'; - if (!customerGroups[name]) { - customerGroups[name] = []; - } - customerGroups[name].push(customer); - }); - - // Render weekly summary - renderWeeklySummary(customerGroups); - } - - // Render weekly summary cards - function renderWeeklySummary(customerGroups) { - const container = document.getElementById('weeklySummaryContainer'); - if (!container) return; - - container.innerHTML = ''; - - const customerNames = Object.keys(customerGroups); - - if (customerNames.length === 0) { - container.innerHTML = ` + if (customerNames.length === 0) { + container.innerHTML = ` 本周暂无客户进度数据 `; - return; - } - - // Sort customer names - customerNames.sort(); - - customerNames.forEach(customerName => { - const records = customerGroups[customerName]; - const card = createCustomerSummaryCard(customerName, records); - container.appendChild(card); - }); + return; } - // Create customer summary card - function createCustomerSummaryCard(customerName, records) { - const card = document.createElement('div'); - card.className = 'customer-summary-card'; + // Sort customer names + customerNames.sort(); - // Sort records by date - records.sort((a, b) => { - const dateA = parseCustomerDate(a.intendedProduct) || new Date(0); - const dateB = parseCustomerDate(b.intendedProduct) || new Date(0); - return dateB - dateA; - }); + customerNames.forEach((customerName) => { + const records = customerGroups[customerName]; + const card = createCustomerSummaryCard(customerName, records); + container.appendChild(card); + }); + } - // Count statistics - const totalRecords = records.length; + // Create customer summary card + function createCustomerSummaryCard(customerName, records) { + const card = document.createElement("div"); + card.className = "customer-summary-card"; - // Get unique types and modules - const types = [...new Set(records.map(r => r.type).filter(Boolean))]; - const modules = [...new Set(records.map(r => r.module).filter(Boolean))]; + // Sort records by date + records.sort((a, b) => { + const dateA = parseCustomerDate(a.intendedProduct) || new Date(0); + const dateB = parseCustomerDate(b.intendedProduct) || new Date(0); + return dateB - dateA; + }); - // Build combined items (description + solution in one row) - let combinedItems = ''; - records.forEach(record => { - const date = parseCustomerDate(record.intendedProduct); - const dateDisplay = date ? formatItemDate(date) : ''; - const description = record.description && record.description.trim() ? escapeHtml(record.description) : '-'; - const solution = record.solution && record.solution.trim() ? escapeHtml(record.solution) : '-'; + // Count statistics + const totalRecords = records.length; - combinedItems += ` + // Get unique types and modules + const types = [...new Set(records.map((r) => r.type).filter(Boolean))]; + const modules = [...new Set(records.map((r) => r.module).filter(Boolean))]; + + // Build combined items (description + solution in one row) + let combinedItems = ""; + records.forEach((record) => { + const date = parseCustomerDate(record.intendedProduct); + const dateDisplay = date ? formatItemDate(date) : ""; + const description = + record.description && record.description.trim() + ? escapeHtml(record.description) + : "-"; + const solution = + record.solution && record.solution.trim() + ? escapeHtml(record.solution) + : "-"; + + combinedItems += ` ${dateDisplay} @@ -2737,17 +3004,17 @@ document.addEventListener('DOMContentLoaded', function () { ${solution} - ${record.version ? ` ${escapeHtml(record.version)}` : ''} - ${record.type ? ` ${escapeHtml(record.type)}` : ''} - ${record.module ? ` ${escapeHtml(record.module)}` : ''} - ${record.statusProgress ? ` ${escapeHtml(record.statusProgress)}` : ''} + ${record.version ? ` ${escapeHtml(record.version)}` : ""} + ${record.type ? ` ${escapeHtml(record.type)}` : ""} + ${record.module ? ` ${escapeHtml(record.module)}` : ""} + ${record.statusProgress ? ` ${escapeHtml(record.statusProgress)}` : ""} `; - }); + }); - card.innerHTML = ` + card.innerHTML = ` @@ -2766,106 +3033,115 @@ document.addEventListener('DOMContentLoaded', function () { - 类型: ${types.join(', ') || '-'} + 类型: ${types.join(", ") || "-"} - 模块: ${modules.join(', ') || '-'} + 模块: ${modules.join(", ") || "-"} `; - return card; - } + return card; + } - // Format date for summary item - function formatItemDate(date) { - const dayNames = ['日', '一', '二', '三', '四', '五', '六']; - const month = date.getMonth() + 1; - const day = date.getDate(); - const dayOfWeek = dayNames[date.getDay()]; + // Format date for summary item + function formatItemDate(date) { + const dayNames = ["日", "一", "二", "三", "四", "五", "六"]; + const month = date.getMonth() + 1; + const day = date.getDate(); + const dayOfWeek = dayNames[date.getDay()]; - return ` + return ` ${month}/${day} 周${dayOfWeek} `; - } + } - // Escape HTML to prevent XSS - function escapeHtml(text) { - if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } + // Escape HTML to prevent XSS + function escapeHtml(text) { + if (!text) return ""; + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } - // Export weekly summary - document.getElementById('exportWeeklySummaryBtn')?.addEventListener('click', () => { - const weekRange = getWeekRange(weekOffset); - const weekStart = formatDateForCompare(weekRange.start); - const weekEnd = formatDateForCompare(weekRange.end); + // Export weekly summary + document + .getElementById("exportWeeklySummaryBtn") + ?.addEventListener("click", () => { + const weekRange = getWeekRange(weekOffset); + const weekStart = formatDateForCompare(weekRange.start); + const weekEnd = formatDateForCompare(weekRange.end); - const weeklyCustomers = allCustomers.filter(customer => { - const customerDate = parseCustomerDate(customer.intendedProduct); - if (!customerDate) return false; - const dateStr = formatDateForCompare(customerDate); - return dateStr >= weekStart && dateStr <= weekEnd; - }); + const weeklyCustomers = allCustomers.filter((customer) => { + const customerDate = parseCustomerDate(customer.intendedProduct); + if (!customerDate) return false; + const dateStr = formatDateForCompare(customerDate); + return dateStr >= weekStart && dateStr <= weekEnd; + }); - // Group by customer name - const customerGroups = {}; - weeklyCustomers.forEach(customer => { - const name = customer.customerName || '未知客户'; - if (!customerGroups[name]) { - customerGroups[name] = []; + // Group by customer name + const customerGroups = {}; + weeklyCustomers.forEach((customer) => { + const name = customer.customerName || "未知客户"; + if (!customerGroups[name]) { + customerGroups[name] = []; + } + customerGroups[name].push(customer); + }); + + // Build export content + let content = `周报汇总 (${formatWeekDate(weekRange.start)} - ${formatWeekDate(weekRange.end)})\n`; + content += "=".repeat(60) + "\n\n"; + + Object.keys(customerGroups) + .sort() + .forEach((customerName) => { + const records = customerGroups[customerName]; + content += `【${customerName}】\n`; + content += "-".repeat(40) + "\n"; + + content += "\n▶ 问题描述:\n"; + records.forEach((record) => { + if (record.description && record.description.trim()) { + const date = parseCustomerDate(record.intendedProduct); + const dateStr = date + ? `${date.getMonth() + 1}/${date.getDate()}` + : ""; + content += ` [${dateStr}] ${record.description}\n`; + if (record.version) content += ` 版本: ${record.version}\n`; + if (record.type) content += ` 类型: ${record.type}\n`; + if (record.statusProgress) + content += ` 状态: ${record.statusProgress}\n`; } - customerGroups[name].push(customer); + }); + + content += "\n▶ 解决方案:\n"; + records.forEach((record) => { + if (record.solution && record.solution.trim()) { + const date = parseCustomerDate(record.intendedProduct); + const dateStr = date + ? `${date.getMonth() + 1}/${date.getDate()}` + : ""; + content += ` [${dateStr}] ${record.solution}\n`; + } + }); + + content += "\n"; }); - // Build export content - let content = `周报汇总 (${formatWeekDate(weekRange.start)} - ${formatWeekDate(weekRange.end)})\n`; - content += '='.repeat(60) + '\n\n'; - - Object.keys(customerGroups).sort().forEach(customerName => { - const records = customerGroups[customerName]; - content += `【${customerName}】\n`; - content += '-'.repeat(40) + '\n'; - - content += '\n▶ 问题描述:\n'; - records.forEach(record => { - if (record.description && record.description.trim()) { - const date = parseCustomerDate(record.intendedProduct); - const dateStr = date ? `${date.getMonth() + 1}/${date.getDate()}` : ''; - content += ` [${dateStr}] ${record.description}\n`; - if (record.version) content += ` 版本: ${record.version}\n`; - if (record.type) content += ` 类型: ${record.type}\n`; - if (record.statusProgress) content += ` 状态: ${record.statusProgress}\n`; - } - }); - - content += '\n▶ 解决方案:\n'; - records.forEach(record => { - if (record.solution && record.solution.trim()) { - const date = parseCustomerDate(record.intendedProduct); - const dateStr = date ? `${date.getMonth() + 1}/${date.getDate()}` : ''; - content += ` [${dateStr}] ${record.solution}\n`; - } - }); - - content += '\n'; - }); - - // Download as text file - const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `周报汇总_${formatDateForCompare(weekRange.start)}_${formatDateForCompare(weekRange.end)}.txt`; - document.body.appendChild(a); - a.click(); - a.remove(); - URL.revokeObjectURL(url); + // Download as text file + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `周报汇总_${formatDateForCompare(weekRange.start)}_${formatDateForCompare(weekRange.end)}.txt`; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); }); });
本周暂无客户进度数据