1083 lines
38 KiB
JavaScript
1083 lines
38 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function () {
|
|
// 登录守卫
|
|
const token = localStorage.getItem('crmToken');
|
|
if (!token && !window.location.pathname.endsWith('login.html')) {
|
|
window.location.href = '/static/login.html';
|
|
return;
|
|
}
|
|
|
|
// 封装带 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;
|
|
}
|
|
|
|
// 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;
|
|
|
|
// Current section tracking
|
|
let currentSection = 'customer';
|
|
|
|
// Pagination state
|
|
let currentPage = 1;
|
|
let pageSize = 10;
|
|
let totalPages = 1;
|
|
let totalItems = 0;
|
|
|
|
let selectedCustomerFilter = '';
|
|
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');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add Customer button
|
|
addCustomerBtn.addEventListener('click', function () {
|
|
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();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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');
|
|
});
|
|
|
|
if (section === 'customer') {
|
|
customerSection.classList.add('active');
|
|
dashboardSection.classList.remove('active');
|
|
document.querySelector('[data-section="customer"]').classList.add('active');
|
|
pageTitle.textContent = '客户管理';
|
|
currentSection = 'customer';
|
|
loadCustomers();
|
|
} else if (section === 'dashboard') {
|
|
customerSection.classList.remove('active');
|
|
dashboardSection.classList.add('active');
|
|
document.querySelector('[data-section="dashboard"]').classList.add('active');
|
|
pageTitle.textContent = '数据仪表板';
|
|
currentSection = 'dashboard';
|
|
loadDashboardData();
|
|
}
|
|
|
|
// 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();
|
|
applyAllCustomerFilters();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading customers:', error);
|
|
}
|
|
}
|
|
|
|
// Populate customer filter dropdown
|
|
function populateCustomerFilter() {
|
|
console.log('Populating filter, allCustomers:', allCustomers);
|
|
const uniqueCustomers = [...new Set(allCustomers.map(c => c.customerName).filter(c => c))];
|
|
console.log('Unique customers:', uniqueCustomers);
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 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 (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 || '',
|
|
c.version || '',
|
|
c.description || '',
|
|
c.solution || '',
|
|
c.type || '',
|
|
c.module || '',
|
|
c.statusProgress || '',
|
|
c.reporter || '',
|
|
date
|
|
];
|
|
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', () => {
|
|
if (currentSection === 'customer') {
|
|
loadCustomers();
|
|
}
|
|
});
|
|
}
|
|
|
|
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() {
|
|
console.log('loadAllCustomers called');
|
|
try {
|
|
const response = await authenticatedFetch('/api/customers?page=1&pageSize=1000');
|
|
const data = await response.json();
|
|
|
|
console.log('Dashboard data received:', data);
|
|
|
|
if (data.customers) {
|
|
dashboardCustomers = data.customers;
|
|
console.log('Dashboard customers set:', dashboardCustomers.length, 'customers');
|
|
updateDashboardStats(dashboardCustomers);
|
|
renderStatusChart(dashboardCustomers);
|
|
renderTypeChart(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);
|
|
}
|
|
|
|
// Render customer table
|
|
function renderCustomerTable(customers) {
|
|
customerTableBody.innerHTML = '';
|
|
|
|
customers.forEach(customer => {
|
|
const row = document.createElement('tr');
|
|
|
|
const date = customer.intendedProduct || '';
|
|
|
|
const fields = [
|
|
{ value: customer.customerName || '', name: 'customerName' },
|
|
{ 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.reporter || '', name: 'reporter' },
|
|
{ value: date, name: 'date' }
|
|
];
|
|
|
|
fields.forEach(field => {
|
|
const td = document.createElement('td');
|
|
const textValue = String(field.value ?? '');
|
|
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.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: document.getElementById('createReporter').value
|
|
};
|
|
|
|
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 || '';
|
|
document.getElementById('editReporter').value = customer.reporter || '';
|
|
|
|
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: document.getElementById('editReporter').value
|
|
};
|
|
|
|
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() {
|
|
console.log('loadDashboardData called');
|
|
await loadAllCustomers();
|
|
}
|
|
|
|
// 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 products = new Set(customers.map(c => c.version)).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('totalProducts').textContent = products;
|
|
document.getElementById('completedTasks').textContent = completed;
|
|
}
|
|
|
|
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: 'bottom'
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: chartTitle,
|
|
font: {
|
|
size: 16,
|
|
weight: 'bold'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Render customer type chart
|
|
function renderTypeChart(customers) {
|
|
console.log('renderTypeChart called with customers:', customers);
|
|
const canvas = document.getElementById('typeChart');
|
|
console.log('typeChart canvas element:', canvas);
|
|
|
|
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);
|
|
|
|
console.log('Type chart labels:', labels);
|
|
console.log('Type chart data:', data);
|
|
|
|
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: 'bottom'
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: chartTitle,
|
|
font: {
|
|
size: 16,
|
|
weight: 'bold'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log('Type chart created successfully');
|
|
}
|
|
|
|
// Initialize the app
|
|
loadCustomers().then(() => {
|
|
loadAllCustomers();
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
|
|
// 登出功能
|
|
const logoutBtn = document.createElement('button');
|
|
logoutBtn.className = 'icon-btn';
|
|
logoutBtn.innerHTML = '<i class="fas fa-sign-out-alt"></i>';
|
|
logoutBtn.title = '登出';
|
|
logoutBtn.addEventListener('click', () => {
|
|
if (confirm('确定要退出登录吗?')) {
|
|
localStorage.removeItem('crmToken');
|
|
window.location.href = '/static/login.html';
|
|
}
|
|
});
|
|
|
|
const headerRight = document.querySelector('.header-right');
|
|
if (headerRight) {
|
|
headerRight.appendChild(logoutBtn);
|
|
}
|
|
});
|