// 封装带 Token 的 fetch (全局函数) async function authenticatedFetch(url, options = {}) { const token = localStorage.getItem("crmToken"); const headers = { ...options.headers, Authorization: `Bearer ${token}`, }; const response = await fetch(url, { ...options, headers }); if (response.status === 401) { localStorage.removeItem("crmToken"); window.location.href = "/static/login.html"; return; } 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; } } // 获取当前用户角色 function getUserRole() { 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; } // 检查当前用户是否可以删除数据 // admin 用户 (role: viewer) 不能删除 // administrator 用户 (role: admin) 可以删除 function canDelete() { 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"; 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 createUploadedScreenshots = []; let editUploadedScreenshots = []; 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; } positionCellTooltipForCell(cell); } 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; 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; currentPage = 1; applyAllCustomerFilters(); }); 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); }); // Chart title change events - 添加空值检查防止元素不存在时报错 const statusChartTitleEl = document.getElementById("statusChartTitle"); if (statusChartTitleEl) { statusChartTitleEl.addEventListener("input", function () { applyDateFilter( document.getElementById("startDate").value, document.getElementById("endDate").value, ); }); } const typeChartTitleEl = document.getElementById("typeChartTitle"); if (typeChartTitleEl) { typeChartTitleEl.addEventListener("input", function () { applyDateFilter( document.getElementById("startDate").value, document.getElementById("endDate").value, ); }); } // Switch between sections function switchSection(section) { navItems.forEach((item) => { item.classList.remove("active"); }); // 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"); } // Re-trigger animate-up animation for visible cards setTimeout(() => { const activeSection = document.querySelector('.content-section.active'); if (activeSection) { const animatedCards = activeSection.querySelectorAll('.animate-up'); animatedCards.forEach(card => { // Remove and re-add animation class to trigger animation card.style.animation = 'none'; // Force reflow card.offsetHeight; card.style.animation = ''; }); } }, 10); } // 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); } } // 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); } if (selectedTypeFilter) { next = next.filter((c) => c.type === selectedTypeFilter); } if (customerStartDate) { next = next.filter((c) => { const date = normalizeDateValue(c.intendedProduct); return date && date >= customerStartDate; }); } 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" }, { value: customer.screenshots || [], name: "screenshots" }, ]; 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 if (field.name === "screenshots") { td.classList.add("screenshot-column-cell"); renderScreenshotsInCell(td, field.value); } 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 = ` `; row.appendChild(actionTd); customerTableBody.appendChild(row); }); checkTextOverflow(); initImageViewer(); 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 screenshots: createUploadedScreenshots, }; try { const response = await authenticatedFetch("/api/customers", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(formData), }); if (response.ok) { createCustomerForm.reset(); createUploadedScreenshots = []; renderScreenshotPreviews("create", []); // 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; } 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("导入客户时出错"); } }); // 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 || ""; // Screenshots editUploadedScreenshots = customer.screenshots || []; renderScreenshotPreviews("edit", editUploadedScreenshots); // 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"; } }); // 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 screenshots: editUploadedScreenshots, }; 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; } try { const response = await authenticatedFetch( `/api/customers/${customerId}`, { method: "DELETE", }, ); if (response.ok) { loadCustomers(); } else { alert("删除客户时出错"); } } catch (error) { console.error("Error deleting customer:", error); alert("删除客户时出错"); } } // 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 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, }; }); } return []; }, }, }, title: { display: false, 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: false, 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); } } // Create follow-up if (followupForm) { followupForm.addEventListener("submit", async function (e) { e.preventDefault(); 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 = ""; 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", }); // 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级 (一般客户)"; } row.innerHTML = `
本周暂无客户进度数据
暂无数据