feat: Add new server binaries and reorder 'Time' column in customer table and CSV export.
This commit is contained in:
parent
9e8c8ec8f2
commit
2b50b3a2f7
@ -107,7 +107,6 @@
|
||||
<table id="customerTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>客户</th>
|
||||
<th>版本</th>
|
||||
<th>描述</th>
|
||||
@ -116,6 +115,7 @@
|
||||
<th>模块</th>
|
||||
<th>状态与进度</th>
|
||||
<th>报告人</th>
|
||||
<th>时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// 登录守卫
|
||||
const token = localStorage.getItem('crmToken');
|
||||
if (!token && !window.location.pathname.endsWith('login.html')) {
|
||||
@ -15,13 +15,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
};
|
||||
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
|
||||
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('crmToken');
|
||||
window.location.href = '/static/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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');
|
||||
@ -48,18 +48,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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;
|
||||
@ -198,88 +198,88 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const day = parts[2].padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
|
||||
// Navigation event listeners
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
item.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const section = this.getAttribute('data-section');
|
||||
switchSection(section);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Menu toggle for mobile
|
||||
menuToggle.addEventListener('click', function() {
|
||||
menuToggle.addEventListener('click', function () {
|
||||
sidebar.classList.toggle('open');
|
||||
});
|
||||
|
||||
|
||||
// Close sidebar when clicking outside on mobile
|
||||
document.addEventListener('click', function(e) {
|
||||
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() {
|
||||
addCustomerBtn.addEventListener('click', function () {
|
||||
createModal.style.display = 'block';
|
||||
});
|
||||
|
||||
|
||||
// Import button
|
||||
importBtn.addEventListener('click', function() {
|
||||
importBtn.addEventListener('click', function () {
|
||||
importModal.style.display = 'block';
|
||||
});
|
||||
|
||||
|
||||
// Close create modal
|
||||
createModal.querySelector('.close').addEventListener('click', function() {
|
||||
createModal.querySelector('.close').addEventListener('click', function () {
|
||||
createModal.style.display = 'none';
|
||||
});
|
||||
|
||||
createModal.querySelector('.cancel-create').addEventListener('click', function() {
|
||||
|
||||
createModal.querySelector('.cancel-create').addEventListener('click', function () {
|
||||
createModal.style.display = 'none';
|
||||
});
|
||||
|
||||
|
||||
// Close import modal
|
||||
importModal.querySelector('.close').addEventListener('click', function() {
|
||||
importModal.querySelector('.close').addEventListener('click', function () {
|
||||
importModal.style.display = 'none';
|
||||
});
|
||||
|
||||
importModal.querySelector('.cancel-import').addEventListener('click', function() {
|
||||
|
||||
importModal.querySelector('.cancel-import').addEventListener('click', function () {
|
||||
importModal.style.display = 'none';
|
||||
});
|
||||
|
||||
|
||||
// Close edit modal
|
||||
editModal.querySelector('.close').addEventListener('click', function() {
|
||||
editModal.querySelector('.close').addEventListener('click', function () {
|
||||
editModal.style.display = 'none';
|
||||
});
|
||||
|
||||
editModal.querySelector('.cancel-edit').addEventListener('click', function() {
|
||||
|
||||
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() {
|
||||
|
||||
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() {
|
||||
customerFilter.addEventListener('change', function () {
|
||||
selectedCustomerFilter = this.value;
|
||||
currentPage = 1;
|
||||
applyAllCustomerFilters();
|
||||
});
|
||||
|
||||
if (customerSearchInput) {
|
||||
customerSearchInput.addEventListener('input', function() {
|
||||
customerSearchInput.addEventListener('input', function () {
|
||||
customerSearchQuery = (this.value || '').trim();
|
||||
if (currentSection === 'customer') {
|
||||
currentPage = 1;
|
||||
@ -287,29 +287,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Apply date filter for dashboard
|
||||
document.getElementById('applyFilters').addEventListener('click', function() {
|
||||
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() {
|
||||
document.getElementById('statusChartTitle').addEventListener('input', function () {
|
||||
applyDateFilter(document.getElementById('startDate').value, document.getElementById('endDate').value);
|
||||
});
|
||||
|
||||
document.getElementById('typeChartTitle').addEventListener('input', function() {
|
||||
|
||||
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');
|
||||
@ -325,19 +325,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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();
|
||||
@ -347,14 +347,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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.intendedProduct).filter(c => c))];
|
||||
console.log('Unique customers:', uniqueCustomers);
|
||||
customerFilter.innerHTML = '<option value="">全部客户</option>';
|
||||
|
||||
|
||||
uniqueCustomers.forEach(customer => {
|
||||
const option = document.createElement('option');
|
||||
option.value = customer;
|
||||
@ -366,7 +366,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
customerFilter.value = selectedCustomerFilter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Filter customers by selected customer
|
||||
function filterCustomers(selectedCustomer) {
|
||||
selectedCustomerFilter = selectedCustomer;
|
||||
@ -438,7 +438,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
function exportCustomersToCsv(customers) {
|
||||
const header = [
|
||||
'时间',
|
||||
'客户',
|
||||
'版本',
|
||||
'描述',
|
||||
@ -446,13 +445,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
'类型',
|
||||
'模块',
|
||||
'状态与进度',
|
||||
'报告人'
|
||||
'报告人',
|
||||
'时间'
|
||||
].map(toCsvCell).join(',');
|
||||
|
||||
const lines = customers.map(c => {
|
||||
const date = normalizeDateValue(c.customerName) || (c.customerName || '');
|
||||
const cells = [
|
||||
date,
|
||||
c.intendedProduct || '',
|
||||
c.version || '',
|
||||
c.description || '',
|
||||
@ -460,7 +459,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
c.type || '',
|
||||
c.module || '',
|
||||
c.statusProgress || '',
|
||||
c.reporter || ''
|
||||
c.reporter || '',
|
||||
date
|
||||
];
|
||||
return cells.map(toCsvCell).join(',');
|
||||
});
|
||||
@ -491,29 +491,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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');
|
||||
@ -527,11 +527,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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.customerName) return false;
|
||||
@ -539,7 +539,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return date && date >= startDate;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (endDate) {
|
||||
filteredData = filteredData.filter(c => {
|
||||
if (!c.customerName) return false;
|
||||
@ -547,23 +547,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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.customerName || '';
|
||||
|
||||
|
||||
const fields = [
|
||||
{ value: date, name: 'date' },
|
||||
{ value: customer.intendedProduct || '', name: 'intendedProduct' },
|
||||
{ value: customer.version || '', name: 'version' },
|
||||
{ value: customer.description || '', name: 'description' },
|
||||
@ -571,22 +570,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
{ value: customer.type || '', name: 'type' },
|
||||
{ value: customer.module || '', name: 'module' },
|
||||
{ value: customer.statusProgress || '', name: 'statusProgress' },
|
||||
{ value: customer.reporter || '', name: 'reporter' }
|
||||
{ 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}">
|
||||
@ -597,27 +597,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</button>
|
||||
`;
|
||||
row.appendChild(actionTd);
|
||||
|
||||
|
||||
customerTableBody.appendChild(row);
|
||||
});
|
||||
|
||||
|
||||
checkTextOverflow();
|
||||
|
||||
|
||||
document.querySelectorAll('.edit-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
btn.addEventListener('click', function () {
|
||||
const customerId = this.getAttribute('data-id');
|
||||
openEditModal(customerId);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.querySelectorAll('.delete-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
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(() => {
|
||||
@ -635,31 +635,31 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 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 =
|
||||
|
||||
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' : ''}`;
|
||||
@ -671,11 +671,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
pageNumbers.appendChild(pageBtn);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create customer
|
||||
createCustomerForm.addEventListener('submit', async function(e) {
|
||||
createCustomerForm.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
const formData = {
|
||||
customerName: document.getElementById('customerName').value,
|
||||
intendedProduct: document.getElementById('intendedProduct').value,
|
||||
@ -687,7 +687,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
statusProgress: document.getElementById('createStatusProgress').value,
|
||||
reporter: document.getElementById('createReporter').value
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch('/api/customers', {
|
||||
method: 'POST',
|
||||
@ -696,7 +696,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
createCustomerForm.reset();
|
||||
createModal.style.display = 'none';
|
||||
@ -710,27 +710,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
alert('创建客户时出错');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Import customers
|
||||
importFileForm.addEventListener('submit', async function(e) {
|
||||
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}`;
|
||||
@ -747,13 +747,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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 = normalizeDateValue(customer.customerName);
|
||||
document.getElementById('editIntendedProduct').value = customer.intendedProduct || '';
|
||||
@ -764,14 +764,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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) {
|
||||
|
||||
window.addEventListener('click', function (e) {
|
||||
if (e.target === createModal) {
|
||||
createModal.style.display = 'none';
|
||||
}
|
||||
@ -782,13 +782,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
editModal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Update customer
|
||||
editCustomerForm.addEventListener('submit', async function(e) {
|
||||
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,
|
||||
@ -800,7 +800,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
statusProgress: document.getElementById('editStatusProgress').value,
|
||||
reporter: document.getElementById('editReporter').value
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(`/api/customers/${customerId}`, {
|
||||
method: 'PUT',
|
||||
@ -809,7 +809,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
editModal.style.display = 'none';
|
||||
loadCustomers();
|
||||
@ -822,18 +822,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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('客户删除成功!');
|
||||
@ -845,17 +845,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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.intendedProduct).filter(c => c)).size;
|
||||
|
||||
|
||||
const now = new Date();
|
||||
const currentMonth = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const currentYear = now.getFullYear();
|
||||
@ -869,39 +869,39 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return year === currentYear && month === currentMonth;
|
||||
}).map(c => c.intendedProduct).filter(c => c)
|
||||
).size;
|
||||
|
||||
|
||||
const products = new Set(customers.map(c => c.intendedProduct)).size;
|
||||
|
||||
|
||||
const completed = new Set(
|
||||
customers.filter(c =>
|
||||
customers.filter(c =>
|
||||
c.statusProgress && (c.statusProgress.includes('已修复') || c.statusProgress.includes('完成') || c.statusProgress.toLowerCase().includes('complete'))
|
||||
).map(c => c.intendedProduct).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: {
|
||||
@ -940,38 +940,38 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 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: {
|
||||
@ -1005,15 +1005,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
console.log('Type chart created successfully');
|
||||
}
|
||||
|
||||
|
||||
// Initialize the app
|
||||
loadCustomers().then(() => {
|
||||
loadAllCustomers();
|
||||
});
|
||||
|
||||
|
||||
// Pagination event listeners
|
||||
document.getElementById('firstPage').addEventListener('click', () => {
|
||||
if (currentPage > 1) {
|
||||
@ -1021,43 +1021,43 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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() {
|
||||
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() {
|
||||
document.getElementById('typeChartFieldSelect').addEventListener('change', function () {
|
||||
const startDate = document.getElementById('startDate').value;
|
||||
const endDate = document.getElementById('endDate').value;
|
||||
applyDateFilter(startDate, endDate);
|
||||
@ -1074,7 +1074,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
window.location.href = '/static/login.html';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const headerRight = document.querySelector('.header-right');
|
||||
if (headerRight) {
|
||||
headerRight.appendChild(logoutBtn);
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"crm-go/models"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
"crm-go/models"
|
||||
)
|
||||
|
||||
type CustomerStorage interface {
|
||||
@ -31,58 +31,72 @@ func NewCustomerStorage(filePath string) CustomerStorage {
|
||||
storage := &customerStorage{
|
||||
filePath: filePath,
|
||||
}
|
||||
|
||||
|
||||
// Ensure the directory exists
|
||||
dir := os.DirFS(filePath[:len(filePath)-len("/customers.json")])
|
||||
_ = dir // Use the directory to ensure it exists
|
||||
|
||||
|
||||
return storage
|
||||
}
|
||||
|
||||
func (cs *customerStorage) GetAllCustomers() ([]models.Customer, error) {
|
||||
cs.mutex.RLock()
|
||||
defer cs.mutex.RUnlock()
|
||||
|
||||
return cs.LoadCustomers()
|
||||
|
||||
customers, err := cs.LoadCustomers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sort by CreatedAt in descending order (newest first)
|
||||
for i := 0; i < len(customers)-1; i++ {
|
||||
for j := i + 1; j < len(customers); j++ {
|
||||
if customers[i].CreatedAt.Before(customers[j].CreatedAt) {
|
||||
customers[i], customers[j] = customers[j], customers[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return customers, nil
|
||||
}
|
||||
|
||||
func (cs *customerStorage) GetCustomerByID(id string) (*models.Customer, error) {
|
||||
cs.mutex.RLock()
|
||||
defer cs.mutex.RUnlock()
|
||||
|
||||
|
||||
customers, err := cs.LoadCustomers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
for _, customer := range customers {
|
||||
if customer.ID == id {
|
||||
return &customer, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (cs *customerStorage) CreateCustomer(customer models.Customer) error {
|
||||
cs.mutex.Lock()
|
||||
defer cs.mutex.Unlock()
|
||||
|
||||
|
||||
if customer.ID == "" {
|
||||
customer.ID = generateUUID()
|
||||
}
|
||||
|
||||
|
||||
if customer.CreatedAt.IsZero() {
|
||||
customer.CreatedAt = time.Now()
|
||||
}
|
||||
|
||||
|
||||
customers, err := cs.LoadCustomers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
customers = append(customers, customer)
|
||||
|
||||
|
||||
return cs.SaveCustomers(customers)
|
||||
}
|
||||
|
||||
@ -91,19 +105,19 @@ func generateUUID() string {
|
||||
rand.Read(bytes)
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40 // Version 4
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80 // Variant
|
||||
|
||||
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func (cs *customerStorage) UpdateCustomer(id string, updates models.UpdateCustomerRequest) error {
|
||||
cs.mutex.Lock()
|
||||
defer cs.mutex.Unlock()
|
||||
|
||||
|
||||
customers, err := cs.LoadCustomers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
for i, customer := range customers {
|
||||
if customer.ID == id {
|
||||
if updates.CustomerName != nil {
|
||||
@ -133,30 +147,30 @@ func (cs *customerStorage) UpdateCustomer(id string, updates models.UpdateCustom
|
||||
if updates.Reporter != nil {
|
||||
customers[i].Reporter = *updates.Reporter
|
||||
}
|
||||
|
||||
|
||||
return cs.SaveCustomers(customers)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil // Customer not found, but not an error
|
||||
}
|
||||
|
||||
func (cs *customerStorage) DeleteCustomer(id string) error {
|
||||
cs.mutex.Lock()
|
||||
defer cs.mutex.Unlock()
|
||||
|
||||
|
||||
customers, err := cs.LoadCustomers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
for i, customer := range customers {
|
||||
if customer.ID == id {
|
||||
customers = append(customers[:i], customers[i+1:]...)
|
||||
return cs.SaveCustomers(customers)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil // Customer not found, but not an error
|
||||
}
|
||||
|
||||
@ -166,12 +180,12 @@ func (cs *customerStorage) SaveCustomers(customers []models.Customer) error {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
data, err := json.MarshalIndent(customers, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
return os.WriteFile(cs.filePath, data, 0644)
|
||||
}
|
||||
|
||||
@ -181,34 +195,34 @@ func (cs *customerStorage) LoadCustomers() ([]models.Customer, error) {
|
||||
// Return empty slice if file doesn't exist
|
||||
return []models.Customer{}, nil
|
||||
}
|
||||
|
||||
|
||||
data, err := os.ReadFile(cs.filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
var customers []models.Customer
|
||||
if err := json.Unmarshal(data, &customers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return customers, nil
|
||||
}
|
||||
|
||||
func (cs *customerStorage) CustomerExists(customer models.Customer) (bool, error) {
|
||||
cs.mutex.RLock()
|
||||
defer cs.mutex.RUnlock()
|
||||
|
||||
|
||||
customers, err := cs.LoadCustomers()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
||||
for _, existingCustomer := range customers {
|
||||
if existingCustomer.Description == customer.Description {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user