crm/frontend/js/main.js
2026-01-16 16:28:50 +08:00

2205 lines
83 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 封装带 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;
}
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;
}
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();
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 = '<option value="">请选择客户</option>';
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
document.getElementById('statusChartTitle').addEventListener('input', function () {
applyDateFilter(document.getElementById('startDate').value, document.getElementById('endDate').value);
});
document.getElementById('typeChartTitle').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');
}
}
// 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 = '<option value="">全部客户</option>';
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 = '<option value="">全部类型</option>';
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 `<span class="progress-badge progress-completed"><i class="fas fa-check"></i> ${status}</span>`;
} else if (status === '进行中' || statusLower.includes('进行')) {
return `<span class="progress-badge progress-in-progress"><i class="fas fa-sync"></i> ${status}</span>`;
} else if (status === '待排期' || statusLower.includes('待')) {
return `<span class="progress-badge progress-pending"><i class="fas fa-clock"></i> ${status}</span>`;
} else if (status === '已驳回' || statusLower.includes('驳回')) {
return `<span class="progress-badge progress-rejected"><i class="fas fa-times"></i> ${status}</span>`;
} else if (status === '已上线' || statusLower.includes('上线')) {
return `<span class="progress-badge progress-launched"><i class="fas fa-rocket"></i> ${status}</span>`;
}
return `<span class="progress-badge">${status}</span>`;
}
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 = `<strong>${textValue}</strong>`;
} 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');
actionTd.innerHTML = `
<button class="action-btn edit-btn" data-id="${customer.id}">
<i class="fas fa-edit"></i>
</button>
<button class="action-btn delete-btn" data-id="${customer.id}">
<i class="fas fa-trash"></i>
</button>
`;
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').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: document.getElementById('createModule').value,
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();
createModal.style.display = 'none';
loadCustomers();
alert('客户创建成功!');
} 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 || '';
document.getElementById('editModule').value = 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';
}
});
// 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: document.getElementById('editModule').value,
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();
alert('客户更新成功!');
} 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();
alert('客户删除成功!');
} 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 - only include valid customer names
const trialCustomerNames = new Set();
trialPeriods.forEach(period => {
// Only add if customerId exists in map (valid customer)
const customerName = customersMap[period.customerId];
if (customerName) {
trialCustomerNames.add(customerName);
}
// If customerId not in map, skip it (deleted customer) - don't add UUID as name
});
// Update total customers to include trial customers
updateTotalCustomersWithTrialData(trialCustomerNames);
} catch (error) {
console.error('Error loading trial customers for dashboard:', error);
}
}
// Update total customers count including trial customers
function updateTotalCustomersWithTrialData(trialCustomerNames) {
// Get current dashboard customers
const progressCustomerNames = new Set(dashboardCustomers.map(c => c.customerName).filter(c => c));
// Merge both sets
const allCustomerNames = new Set([...progressCustomerNames, ...trialCustomerNames]);
// Update the total customers display
document.getElementById('totalCustomers').textContent = allCustomerNames.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;
const completed = new Set(
customers.filter(c =>
c.statusProgress && (c.statusProgress.includes('已修复') || c.statusProgress.includes('完成') || c.statusProgress.toLowerCase().includes('complete'))
).map(c => c.customerName).filter(c => c)
).size;
document.getElementById('totalCustomers').textContent = totalCustomers;
document.getElementById('newCustomers').textContent = newCustomers;
document.getElementById('completedTasks').textContent = completed;
}
// 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;
} else {
document.getElementById('followUpCount').textContent = '0';
}
} catch (error) {
console.error('Error loading follow-up count:', error);
document.getElementById('followUpCount').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;
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;
// Show/hide follow-up form
if (addFollowUpBtn) {
addFollowUpBtn.addEventListener('click', async function () {
followupFormCard.style.display = 'block';
await loadCustomerListForFollowup();
});
}
if (cancelFollowupBtn) {
cancelFollowupBtn.addEventListener('click', function () {
followupFormCard.style.display = 'none';
followupForm.reset();
});
}
// Load customer list for follow-up dropdown from trial periods
async function loadCustomerListForFollowup() {
try {
const response = await authenticatedFetch('/api/trial-customers/list');
const data = await response.json();
followupCustomerNameSelect.innerHTML = '<option value="">请选择客户</option>';
if (data.customerNames && data.customerNames.length > 0) {
data.customerNames.forEach(customerName => {
const option = document.createElement('option');
option.value = customerName;
option.textContent = customerName;
followupCustomerNameSelect.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();
alert('跟进记录创建成功!');
} 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 ?
'<span style="color: green;">已通知</span>' :
'<span style="color: orange;">待通知</span>';
// 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 = `
<td>${followUp.customerName || ''}</td>
<td>${followUp.dealStatus || ''}</td>
<td>${levelDisplay}</td>
<td>${followUp.industry || ''}</td>
<td>${formattedTime}</td>
<td>${notificationStatus}</td>
<td>
<button class="action-btn delete-followup-btn" data-id="${followUp.id}">
<i class="fas fa-trash"></i>
</button>
</td>
`;
followupTableBody.appendChild(row);
});
// Add delete event listeners
document.querySelectorAll('.delete-followup-btn').forEach(btn => {
btn.addEventListener('click', function () {
const followUpId = this.getAttribute('data-id');
deleteFollowUp(followUpId);
});
});
}
// Delete follow-up
async function deleteFollowUp(followUpId) {
if (!confirm('确定要删除这条跟进记录吗?')) {
return;
}
try {
const response = await authenticatedFetch(`/api/followups/${followUpId}`, {
method: 'DELETE'
});
if (response.ok) {
loadFollowUps();
alert('跟进记录删除成功!');
} else {
alert('删除跟进记录时出错');
}
} catch (error) {
console.error('Error deleting 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);
}
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);
}
}
// Follow-up pagination event listeners
if (document.getElementById('followupFirstPage')) {
document.getElementById('followupFirstPage').addEventListener('click', () => {
if (followupCurrentPage > 1) {
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 = '<div class="search-empty">暂无搜索历史</div>';
return;
}
searchHistoryItems.innerHTML = searchHistory.slice(0, 5).map(item => `
<div class="search-history-item" data-query="${item}">
<i class="fas fa-history"></i>
<span>${item}</span>
</div>
`).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);
});
});
}
// 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 = '<div class="search-empty">输入关键词开始搜索...</div>';
return;
}
searchResultsItems.innerHTML = '<div class="search-empty"><i class="fas fa-spinner fa-spin"></i> 搜索中...</div>';
try {
const results = [];
// Search in trial periods
const trialResponse = await authenticatedFetch('/api/trial-periods/all');
const trialData = await trialResponse.json();
const trialPeriods = trialData.trialPeriods || [];
// Get customers map
const customersResponse = await authenticatedFetch('/api/customers/list');
const customersData = await customersResponse.json();
const customersMap = customersData.customerMap || {};
// Search trial periods
trialPeriods.forEach(period => {
const customerName = customersMap[period.customerId] || period.customerId;
if (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'
});
}
});
// Search in customers (weekly progress)
const progressResponse = await authenticatedFetch('/api/customers?page=1&pageSize=100');
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'
});
}
});
// Search in followups
const followupResponse = await authenticatedFetch('/api/followups');
const followupData = await followupResponse.json();
const followups = followupData.followups || [];
followups.forEach(followup => {
if (followup.customerName.toLowerCase().includes(query.toLowerCase())) {
results.push({
type: 'followup',
title: followup.customerName,
meta: `状态: ${followup.dealStatus || '未知'} | 级别: ${followup.customerLevel || '未知'}`,
icon: 'fas fa-tasks',
section: 'followup'
});
}
});
// Render results
if (results.length === 0) {
searchResultsItems.innerHTML = '<div class="search-empty">未找到匹配结果</div>';
} else {
searchResultsItems.innerHTML = results.slice(0, 10).map(result => `
<div class="search-result-item" data-section="${result.section}">
<i class="${result.icon}"></i>
<div class="search-result-info">
<div class="search-result-title">${result.title}</div>
<div class="search-result-meta">${result.meta}</div>
</div>
<span class="search-result-badge ${result.type}">${result.type === 'trial' ? '试用' : result.type === 'progress' ? '进度' : '跟进'}</span>
</div>
`).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 = '<div class="search-empty">搜索出错,请重试</div>';
}
}
// 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);
}
});
});
// 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';
}
});
}
});