feat: Add new server binaries and reorder 'Time' column in customer table and CSV export.

This commit is contained in:
hangyu.tao 2026-01-05 20:09:33 +08:00
parent 9e8c8ec8f2
commit 2b50b3a2f7
3 changed files with 193 additions and 179 deletions

View File

@ -107,7 +107,6 @@
<table id="customerTable"> <table id="customerTable">
<thead> <thead>
<tr> <tr>
<th>时间</th>
<th>客户</th> <th>客户</th>
<th>版本</th> <th>版本</th>
<th>描述</th> <th>描述</th>
@ -116,6 +115,7 @@
<th>模块</th> <th>模块</th>
<th>状态与进度</th> <th>状态与进度</th>
<th>报告人</th> <th>报告人</th>
<th>时间</th>
<th>操作</th> <th>操作</th>
</tr> </tr>
</thead> </thead>

View File

@ -1,4 +1,4 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
// 登录守卫 // 登录守卫
const token = localStorage.getItem('crmToken'); const token = localStorage.getItem('crmToken');
if (!token && !window.location.pathname.endsWith('login.html')) { if (!token && !window.location.pathname.endsWith('login.html')) {
@ -15,13 +15,13 @@ document.addEventListener('DOMContentLoaded', function() {
}; };
const response = await fetch(url, { ...options, headers }); const response = await fetch(url, { ...options, headers });
if (response.status === 401) { if (response.status === 401) {
localStorage.removeItem('crmToken'); localStorage.removeItem('crmToken');
window.location.href = '/static/login.html'; window.location.href = '/static/login.html';
return; return;
} }
return response; return response;
} }
@ -30,7 +30,7 @@ document.addEventListener('DOMContentLoaded', function() {
const customerSection = document.getElementById('customerSection'); const customerSection = document.getElementById('customerSection');
const dashboardSection = document.getElementById('dashboardSection'); const dashboardSection = document.getElementById('dashboardSection');
const pageTitle = document.getElementById('pageTitle'); const pageTitle = document.getElementById('pageTitle');
// Elements // Elements
const createCustomerForm = document.getElementById('createCustomerForm'); const createCustomerForm = document.getElementById('createCustomerForm');
const importFileForm = document.getElementById('importFileForm'); const importFileForm = document.getElementById('importFileForm');
@ -48,18 +48,18 @@ document.addEventListener('DOMContentLoaded', function() {
const contentArea = document.querySelector('.content-area'); const contentArea = document.querySelector('.content-area');
const refreshCustomersBtn = document.getElementById('refreshCustomersBtn'); const refreshCustomersBtn = document.getElementById('refreshCustomersBtn');
const exportCustomersBtn = document.getElementById('exportCustomersBtn'); const exportCustomersBtn = document.getElementById('exportCustomersBtn');
let allCustomers = []; let allCustomers = [];
let filteredCustomers = []; let filteredCustomers = [];
let dashboardCustomers = []; let dashboardCustomers = [];
// Chart instances // Chart instances
let statusChartInstance = null; let statusChartInstance = null;
let typeChartInstance = null; let typeChartInstance = null;
// Current section tracking // Current section tracking
let currentSection = 'customer'; let currentSection = 'customer';
// Pagination state // Pagination state
let currentPage = 1; let currentPage = 1;
let pageSize = 10; let pageSize = 10;
@ -198,88 +198,88 @@ document.addEventListener('DOMContentLoaded', function() {
const day = parts[2].padStart(2, '0'); const day = parts[2].padStart(2, '0');
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
} }
// Navigation event listeners // Navigation event listeners
navItems.forEach(item => { navItems.forEach(item => {
item.addEventListener('click', function(e) { item.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
const section = this.getAttribute('data-section'); const section = this.getAttribute('data-section');
switchSection(section); switchSection(section);
}); });
}); });
// Menu toggle for mobile // Menu toggle for mobile
menuToggle.addEventListener('click', function() { menuToggle.addEventListener('click', function () {
sidebar.classList.toggle('open'); sidebar.classList.toggle('open');
}); });
// Close sidebar when clicking outside on mobile // Close sidebar when clicking outside on mobile
document.addEventListener('click', function(e) { document.addEventListener('click', function (e) {
if (window.innerWidth <= 768) { if (window.innerWidth <= 768) {
if (!sidebar.contains(e.target) && !menuToggle.contains(e.target)) { if (!sidebar.contains(e.target) && !menuToggle.contains(e.target)) {
sidebar.classList.remove('open'); sidebar.classList.remove('open');
} }
} }
}); });
// Add Customer button // Add Customer button
addCustomerBtn.addEventListener('click', function() { addCustomerBtn.addEventListener('click', function () {
createModal.style.display = 'block'; createModal.style.display = 'block';
}); });
// Import button // Import button
importBtn.addEventListener('click', function() { importBtn.addEventListener('click', function () {
importModal.style.display = 'block'; importModal.style.display = 'block';
}); });
// Close create modal // Close create modal
createModal.querySelector('.close').addEventListener('click', function() { createModal.querySelector('.close').addEventListener('click', function () {
createModal.style.display = 'none'; createModal.style.display = 'none';
}); });
createModal.querySelector('.cancel-create').addEventListener('click', function() { createModal.querySelector('.cancel-create').addEventListener('click', function () {
createModal.style.display = 'none'; createModal.style.display = 'none';
}); });
// Close import modal // Close import modal
importModal.querySelector('.close').addEventListener('click', function() { importModal.querySelector('.close').addEventListener('click', function () {
importModal.style.display = 'none'; importModal.style.display = 'none';
}); });
importModal.querySelector('.cancel-import').addEventListener('click', function() { importModal.querySelector('.cancel-import').addEventListener('click', function () {
importModal.style.display = 'none'; importModal.style.display = 'none';
}); });
// Close edit modal // Close edit modal
editModal.querySelector('.close').addEventListener('click', function() { editModal.querySelector('.close').addEventListener('click', function () {
editModal.style.display = 'none'; editModal.style.display = 'none';
}); });
editModal.querySelector('.cancel-edit').addEventListener('click', function() { editModal.querySelector('.cancel-edit').addEventListener('click', function () {
editModal.style.display = 'none'; editModal.style.display = 'none';
}); });
// File name display // File name display
const importFile = document.getElementById('importFile'); const importFile = document.getElementById('importFile');
const fileName = document.getElementById('fileName'); const fileName = document.getElementById('fileName');
importFile.addEventListener('change', function() { importFile.addEventListener('change', function () {
if (this.files.length > 0) { if (this.files.length > 0) {
fileName.textContent = this.files[0].name; fileName.textContent = this.files[0].name;
} else { } else {
fileName.textContent = '选择文件...'; fileName.textContent = '选择文件...';
} }
}); });
// Customer filter change event // Customer filter change event
customerFilter.addEventListener('change', function() { customerFilter.addEventListener('change', function () {
selectedCustomerFilter = this.value; selectedCustomerFilter = this.value;
currentPage = 1; currentPage = 1;
applyAllCustomerFilters(); applyAllCustomerFilters();
}); });
if (customerSearchInput) { if (customerSearchInput) {
customerSearchInput.addEventListener('input', function() { customerSearchInput.addEventListener('input', function () {
customerSearchQuery = (this.value || '').trim(); customerSearchQuery = (this.value || '').trim();
if (currentSection === 'customer') { if (currentSection === 'customer') {
currentPage = 1; currentPage = 1;
@ -287,29 +287,29 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
} }
// Apply date filter for dashboard // Apply date filter for dashboard
document.getElementById('applyFilters').addEventListener('click', function() { document.getElementById('applyFilters').addEventListener('click', function () {
const startDate = document.getElementById('startDate').value; const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value; const endDate = document.getElementById('endDate').value;
applyDateFilter(startDate, endDate); applyDateFilter(startDate, endDate);
}); });
// Chart title change events // 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); 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); applyDateFilter(document.getElementById('startDate').value, document.getElementById('endDate').value);
}); });
// Switch between sections // Switch between sections
function switchSection(section) { function switchSection(section) {
navItems.forEach(item => { navItems.forEach(item => {
item.classList.remove('active'); item.classList.remove('active');
}); });
if (section === 'customer') { if (section === 'customer') {
customerSection.classList.add('active'); customerSection.classList.add('active');
dashboardSection.classList.remove('active'); dashboardSection.classList.remove('active');
@ -325,19 +325,19 @@ document.addEventListener('DOMContentLoaded', function() {
currentSection = 'dashboard'; currentSection = 'dashboard';
loadDashboardData(); loadDashboardData();
} }
// Close sidebar on mobile after navigation // Close sidebar on mobile after navigation
if (window.innerWidth <= 768) { if (window.innerWidth <= 768) {
sidebar.classList.remove('open'); sidebar.classList.remove('open');
} }
} }
// Load customers from API // Load customers from API
async function loadCustomers() { async function loadCustomers() {
try { try {
const response = await authenticatedFetch('/api/customers?page=1&pageSize=1000'); const response = await authenticatedFetch('/api/customers?page=1&pageSize=1000');
const data = await response.json(); const data = await response.json();
if (data.customers) { if (data.customers) {
allCustomers = data.customers; allCustomers = data.customers;
populateCustomerFilter(); populateCustomerFilter();
@ -347,14 +347,14 @@ document.addEventListener('DOMContentLoaded', function() {
console.error('Error loading customers:', error); console.error('Error loading customers:', error);
} }
} }
// Populate customer filter dropdown // Populate customer filter dropdown
function populateCustomerFilter() { function populateCustomerFilter() {
console.log('Populating filter, allCustomers:', allCustomers); console.log('Populating filter, allCustomers:', allCustomers);
const uniqueCustomers = [...new Set(allCustomers.map(c => c.intendedProduct).filter(c => c))]; const uniqueCustomers = [...new Set(allCustomers.map(c => c.intendedProduct).filter(c => c))];
console.log('Unique customers:', uniqueCustomers); console.log('Unique customers:', uniqueCustomers);
customerFilter.innerHTML = '<option value="">全部客户</option>'; customerFilter.innerHTML = '<option value="">全部客户</option>';
uniqueCustomers.forEach(customer => { uniqueCustomers.forEach(customer => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = customer; option.value = customer;
@ -366,7 +366,7 @@ document.addEventListener('DOMContentLoaded', function() {
customerFilter.value = selectedCustomerFilter; customerFilter.value = selectedCustomerFilter;
} }
} }
// Filter customers by selected customer // Filter customers by selected customer
function filterCustomers(selectedCustomer) { function filterCustomers(selectedCustomer) {
selectedCustomerFilter = selectedCustomer; selectedCustomerFilter = selectedCustomer;
@ -438,7 +438,6 @@ document.addEventListener('DOMContentLoaded', function() {
function exportCustomersToCsv(customers) { function exportCustomersToCsv(customers) {
const header = [ const header = [
'时间',
'客户', '客户',
'版本', '版本',
'描述', '描述',
@ -446,13 +445,13 @@ document.addEventListener('DOMContentLoaded', function() {
'类型', '类型',
'模块', '模块',
'状态与进度', '状态与进度',
'报告人' '报告人',
'时间'
].map(toCsvCell).join(','); ].map(toCsvCell).join(',');
const lines = customers.map(c => { const lines = customers.map(c => {
const date = normalizeDateValue(c.customerName) || (c.customerName || ''); const date = normalizeDateValue(c.customerName) || (c.customerName || '');
const cells = [ const cells = [
date,
c.intendedProduct || '', c.intendedProduct || '',
c.version || '', c.version || '',
c.description || '', c.description || '',
@ -460,7 +459,8 @@ document.addEventListener('DOMContentLoaded', function() {
c.type || '', c.type || '',
c.module || '', c.module || '',
c.statusProgress || '', c.statusProgress || '',
c.reporter || '' c.reporter || '',
date
]; ];
return cells.map(toCsvCell).join(','); return cells.map(toCsvCell).join(',');
}); });
@ -491,29 +491,29 @@ document.addEventListener('DOMContentLoaded', function() {
exportCustomersToCsv(filteredCustomers); exportCustomersToCsv(filteredCustomers);
}); });
} }
// Apply customer filter and render table with pagination // Apply customer filter and render table with pagination
function applyCustomerFilter() { function applyCustomerFilter() {
totalItems = filteredCustomers.length; totalItems = filteredCustomers.length;
totalPages = Math.ceil(totalItems / pageSize); totalPages = Math.ceil(totalItems / pageSize);
const startIndex = (currentPage - 1) * pageSize; const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize; const endIndex = startIndex + pageSize;
const paginatedCustomers = filteredCustomers.slice(startIndex, endIndex); const paginatedCustomers = filteredCustomers.slice(startIndex, endIndex);
renderCustomerTable(paginatedCustomers); renderCustomerTable(paginatedCustomers);
updatePaginationControls(); updatePaginationControls();
} }
// Load all customers for dashboard // Load all customers for dashboard
async function loadAllCustomers() { async function loadAllCustomers() {
console.log('loadAllCustomers called'); console.log('loadAllCustomers called');
try { try {
const response = await authenticatedFetch('/api/customers?page=1&pageSize=1000'); const response = await authenticatedFetch('/api/customers?page=1&pageSize=1000');
const data = await response.json(); const data = await response.json();
console.log('Dashboard data received:', data); console.log('Dashboard data received:', data);
if (data.customers) { if (data.customers) {
dashboardCustomers = data.customers; dashboardCustomers = data.customers;
console.log('Dashboard customers set:', dashboardCustomers.length, 'customers'); console.log('Dashboard customers set:', dashboardCustomers.length, 'customers');
@ -527,11 +527,11 @@ document.addEventListener('DOMContentLoaded', function() {
console.error('Error loading all customers:', error); console.error('Error loading all customers:', error);
} }
} }
// Apply date filter for dashboard // Apply date filter for dashboard
function applyDateFilter(startDate, endDate) { function applyDateFilter(startDate, endDate) {
let filteredData = dashboardCustomers; let filteredData = dashboardCustomers;
if (startDate) { if (startDate) {
filteredData = filteredData.filter(c => { filteredData = filteredData.filter(c => {
if (!c.customerName) return false; if (!c.customerName) return false;
@ -539,7 +539,7 @@ document.addEventListener('DOMContentLoaded', function() {
return date && date >= startDate; return date && date >= startDate;
}); });
} }
if (endDate) { if (endDate) {
filteredData = filteredData.filter(c => { filteredData = filteredData.filter(c => {
if (!c.customerName) return false; if (!c.customerName) return false;
@ -547,23 +547,22 @@ document.addEventListener('DOMContentLoaded', function() {
return date && date <= endDate; return date && date <= endDate;
}); });
} }
updateDashboardStats(filteredData); updateDashboardStats(filteredData);
renderStatusChart(filteredData); renderStatusChart(filteredData);
renderTypeChart(filteredData); renderTypeChart(filteredData);
} }
// Render customer table // Render customer table
function renderCustomerTable(customers) { function renderCustomerTable(customers) {
customerTableBody.innerHTML = ''; customerTableBody.innerHTML = '';
customers.forEach(customer => { customers.forEach(customer => {
const row = document.createElement('tr'); const row = document.createElement('tr');
const date = customer.customerName || ''; const date = customer.customerName || '';
const fields = [ const fields = [
{ value: date, name: 'date' },
{ value: customer.intendedProduct || '', name: 'intendedProduct' }, { value: customer.intendedProduct || '', name: 'intendedProduct' },
{ value: customer.version || '', name: 'version' }, { value: customer.version || '', name: 'version' },
{ value: customer.description || '', name: 'description' }, { value: customer.description || '', name: 'description' },
@ -571,22 +570,23 @@ document.addEventListener('DOMContentLoaded', function() {
{ value: customer.type || '', name: 'type' }, { value: customer.type || '', name: 'type' },
{ value: customer.module || '', name: 'module' }, { value: customer.module || '', name: 'module' },
{ value: customer.statusProgress || '', name: 'statusProgress' }, { value: customer.statusProgress || '', name: 'statusProgress' },
{ value: customer.reporter || '', name: 'reporter' } { value: customer.reporter || '', name: 'reporter' },
{ value: date, name: 'date' }
]; ];
fields.forEach(field => { fields.forEach(field => {
const td = document.createElement('td'); const td = document.createElement('td');
const textValue = String(field.value ?? ''); const textValue = String(field.value ?? '');
td.textContent = textValue; td.textContent = textValue;
td.setAttribute('data-tooltip', textValue); td.setAttribute('data-tooltip', textValue);
if (field.name === 'description' || field.name === 'solution') { if (field.name === 'description' || field.name === 'solution') {
td.classList.add('overflow-cell'); td.classList.add('overflow-cell');
} }
row.appendChild(td); row.appendChild(td);
}); });
const actionTd = document.createElement('td'); const actionTd = document.createElement('td');
actionTd.innerHTML = ` actionTd.innerHTML = `
<button class="action-btn edit-btn" data-id="${customer.id}"> <button class="action-btn edit-btn" data-id="${customer.id}">
@ -597,27 +597,27 @@ document.addEventListener('DOMContentLoaded', function() {
</button> </button>
`; `;
row.appendChild(actionTd); row.appendChild(actionTd);
customerTableBody.appendChild(row); customerTableBody.appendChild(row);
}); });
checkTextOverflow(); checkTextOverflow();
document.querySelectorAll('.edit-btn').forEach(btn => { document.querySelectorAll('.edit-btn').forEach(btn => {
btn.addEventListener('click', function() { btn.addEventListener('click', function () {
const customerId = this.getAttribute('data-id'); const customerId = this.getAttribute('data-id');
openEditModal(customerId); openEditModal(customerId);
}); });
}); });
document.querySelectorAll('.delete-btn').forEach(btn => { document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', function() { btn.addEventListener('click', function () {
const customerId = this.getAttribute('data-id'); const customerId = this.getAttribute('data-id');
deleteCustomer(customerId); deleteCustomer(customerId);
}); });
}); });
} }
function checkTextOverflow() { function checkTextOverflow() {
// Use requestAnimationFrame to ensure DOM is fully rendered before checking overflow // Use requestAnimationFrame to ensure DOM is fully rendered before checking overflow
requestAnimationFrame(() => { requestAnimationFrame(() => {
@ -635,31 +635,31 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
} }
// Update pagination controls // Update pagination controls
function updatePaginationControls() { function updatePaginationControls() {
const startItem = totalItems === 0 ? 0 : (currentPage - 1) * pageSize + 1; const startItem = totalItems === 0 ? 0 : (currentPage - 1) * pageSize + 1;
const endItem = Math.min(currentPage * pageSize, totalItems); const endItem = Math.min(currentPage * pageSize, totalItems);
document.getElementById('paginationInfo').textContent = document.getElementById('paginationInfo').textContent =
`显示 ${startItem}-${endItem}${totalItems}`; `显示 ${startItem}-${endItem}${totalItems}`;
document.getElementById('firstPage').disabled = currentPage === 1; document.getElementById('firstPage').disabled = currentPage === 1;
document.getElementById('prevPage').disabled = currentPage === 1; document.getElementById('prevPage').disabled = currentPage === 1;
document.getElementById('nextPage').disabled = currentPage === totalPages; document.getElementById('nextPage').disabled = currentPage === totalPages;
document.getElementById('lastPage').disabled = currentPage === totalPages; document.getElementById('lastPage').disabled = currentPage === totalPages;
const pageNumbers = document.getElementById('pageNumbers'); const pageNumbers = document.getElementById('pageNumbers');
pageNumbers.innerHTML = ''; pageNumbers.innerHTML = '';
const maxVisiblePages = 5; const maxVisiblePages = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2)); let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) { if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1); startPage = Math.max(1, endPage - maxVisiblePages + 1);
} }
for (let i = startPage; i <= endPage; i++) { for (let i = startPage; i <= endPage; i++) {
const pageBtn = document.createElement('button'); const pageBtn = document.createElement('button');
pageBtn.className = `page-number ${i === currentPage ? 'active' : ''}`; pageBtn.className = `page-number ${i === currentPage ? 'active' : ''}`;
@ -671,11 +671,11 @@ document.addEventListener('DOMContentLoaded', function() {
pageNumbers.appendChild(pageBtn); pageNumbers.appendChild(pageBtn);
} }
} }
// Create customer // Create customer
createCustomerForm.addEventListener('submit', async function(e) { createCustomerForm.addEventListener('submit', async function (e) {
e.preventDefault(); e.preventDefault();
const formData = { const formData = {
customerName: document.getElementById('customerName').value, customerName: document.getElementById('customerName').value,
intendedProduct: document.getElementById('intendedProduct').value, intendedProduct: document.getElementById('intendedProduct').value,
@ -687,7 +687,7 @@ document.addEventListener('DOMContentLoaded', function() {
statusProgress: document.getElementById('createStatusProgress').value, statusProgress: document.getElementById('createStatusProgress').value,
reporter: document.getElementById('createReporter').value reporter: document.getElementById('createReporter').value
}; };
try { try {
const response = await authenticatedFetch('/api/customers', { const response = await authenticatedFetch('/api/customers', {
method: 'POST', method: 'POST',
@ -696,7 +696,7 @@ document.addEventListener('DOMContentLoaded', function() {
}, },
body: JSON.stringify(formData) body: JSON.stringify(formData)
}); });
if (response.ok) { if (response.ok) {
createCustomerForm.reset(); createCustomerForm.reset();
createModal.style.display = 'none'; createModal.style.display = 'none';
@ -710,27 +710,27 @@ document.addEventListener('DOMContentLoaded', function() {
alert('创建客户时出错'); alert('创建客户时出错');
} }
}); });
// Import customers // Import customers
importFileForm.addEventListener('submit', async function(e) { importFileForm.addEventListener('submit', async function (e) {
e.preventDefault(); e.preventDefault();
const formData = new FormData(); const formData = new FormData();
const fileInput = document.getElementById('importFile'); const fileInput = document.getElementById('importFile');
if (!fileInput.files.length) { if (!fileInput.files.length) {
alert('请选择要导入的文件'); alert('请选择要导入的文件');
return; return;
} }
formData.append('file', fileInput.files[0]); formData.append('file', fileInput.files[0]);
try { try {
const response = await authenticatedFetch('/api/customers/import', { const response = await authenticatedFetch('/api/customers/import', {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
if (response.ok) { if (response.ok) {
const result = await response.json(); const result = await response.json();
const message = `客户导入成功!\n导入数量: ${result.importedCount}\n重复数量: ${result.duplicateCount}`; const message = `客户导入成功!\n导入数量: ${result.importedCount}\n重复数量: ${result.duplicateCount}`;
@ -747,13 +747,13 @@ document.addEventListener('DOMContentLoaded', function() {
alert('导入客户时出错'); alert('导入客户时出错');
} }
}); });
// Open edit modal // Open edit modal
async function openEditModal(customerId) { async function openEditModal(customerId) {
try { try {
const response = await authenticatedFetch(`/api/customers/${customerId}`); const response = await authenticatedFetch(`/api/customers/${customerId}`);
const customer = await response.json(); const customer = await response.json();
document.getElementById('editCustomerId').value = customer.id; document.getElementById('editCustomerId').value = customer.id;
document.getElementById('editCustomerName').value = normalizeDateValue(customer.customerName); document.getElementById('editCustomerName').value = normalizeDateValue(customer.customerName);
document.getElementById('editIntendedProduct').value = customer.intendedProduct || ''; document.getElementById('editIntendedProduct').value = customer.intendedProduct || '';
@ -764,14 +764,14 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('editModule').value = customer.module || ''; document.getElementById('editModule').value = customer.module || '';
document.getElementById('editStatusProgress').value = customer.statusProgress || ''; document.getElementById('editStatusProgress').value = customer.statusProgress || '';
document.getElementById('editReporter').value = customer.reporter || ''; document.getElementById('editReporter').value = customer.reporter || '';
editModal.style.display = 'block'; editModal.style.display = 'block';
} catch (error) { } catch (error) {
console.error('Error loading customer for edit:', error); console.error('Error loading customer for edit:', error);
} }
} }
window.addEventListener('click', function(e) { window.addEventListener('click', function (e) {
if (e.target === createModal) { if (e.target === createModal) {
createModal.style.display = 'none'; createModal.style.display = 'none';
} }
@ -782,13 +782,13 @@ document.addEventListener('DOMContentLoaded', function() {
editModal.style.display = 'none'; editModal.style.display = 'none';
} }
}); });
// Update customer // Update customer
editCustomerForm.addEventListener('submit', async function(e) { editCustomerForm.addEventListener('submit', async function (e) {
e.preventDefault(); e.preventDefault();
const customerId = document.getElementById('editCustomerId').value; const customerId = document.getElementById('editCustomerId').value;
const formData = { const formData = {
customerName: document.getElementById('editCustomerName').value, customerName: document.getElementById('editCustomerName').value,
intendedProduct: document.getElementById('editIntendedProduct').value, intendedProduct: document.getElementById('editIntendedProduct').value,
@ -800,7 +800,7 @@ document.addEventListener('DOMContentLoaded', function() {
statusProgress: document.getElementById('editStatusProgress').value, statusProgress: document.getElementById('editStatusProgress').value,
reporter: document.getElementById('editReporter').value reporter: document.getElementById('editReporter').value
}; };
try { try {
const response = await authenticatedFetch(`/api/customers/${customerId}`, { const response = await authenticatedFetch(`/api/customers/${customerId}`, {
method: 'PUT', method: 'PUT',
@ -809,7 +809,7 @@ document.addEventListener('DOMContentLoaded', function() {
}, },
body: JSON.stringify(formData) body: JSON.stringify(formData)
}); });
if (response.ok) { if (response.ok) {
editModal.style.display = 'none'; editModal.style.display = 'none';
loadCustomers(); loadCustomers();
@ -822,18 +822,18 @@ document.addEventListener('DOMContentLoaded', function() {
alert('更新客户时出错'); alert('更新客户时出错');
} }
}); });
// Delete customer // Delete customer
async function deleteCustomer(customerId) { async function deleteCustomer(customerId) {
if (!confirm('确定要删除这个客户吗?')) { if (!confirm('确定要删除这个客户吗?')) {
return; return;
} }
try { try {
const response = await authenticatedFetch(`/api/customers/${customerId}`, { const response = await authenticatedFetch(`/api/customers/${customerId}`, {
method: 'DELETE' method: 'DELETE'
}); });
if (response.ok) { if (response.ok) {
loadCustomers(); loadCustomers();
alert('客户删除成功!'); alert('客户删除成功!');
@ -845,17 +845,17 @@ document.addEventListener('DOMContentLoaded', function() {
alert('删除客户时出错'); alert('删除客户时出错');
} }
} }
// Dashboard functionality // Dashboard functionality
async function loadDashboardData() { async function loadDashboardData() {
console.log('loadDashboardData called'); console.log('loadDashboardData called');
await loadAllCustomers(); await loadAllCustomers();
} }
// Update dashboard statistics // Update dashboard statistics
function updateDashboardStats(customers) { function updateDashboardStats(customers) {
const totalCustomers = new Set(customers.map(c => c.intendedProduct).filter(c => c)).size; const totalCustomers = new Set(customers.map(c => c.intendedProduct).filter(c => c)).size;
const now = new Date(); const now = new Date();
const currentMonth = String(now.getMonth() + 1).padStart(2, '0'); const currentMonth = String(now.getMonth() + 1).padStart(2, '0');
const currentYear = now.getFullYear(); const currentYear = now.getFullYear();
@ -869,39 +869,39 @@ document.addEventListener('DOMContentLoaded', function() {
return year === currentYear && month === currentMonth; return year === currentYear && month === currentMonth;
}).map(c => c.intendedProduct).filter(c => c) }).map(c => c.intendedProduct).filter(c => c)
).size; ).size;
const products = new Set(customers.map(c => c.intendedProduct)).size; const products = new Set(customers.map(c => c.intendedProduct)).size;
const completed = new Set( const completed = new Set(
customers.filter(c => customers.filter(c =>
c.statusProgress && (c.statusProgress.includes('已修复') || c.statusProgress.includes('完成') || c.statusProgress.toLowerCase().includes('complete')) c.statusProgress && (c.statusProgress.includes('已修复') || c.statusProgress.includes('完成') || c.statusProgress.toLowerCase().includes('complete'))
).map(c => c.intendedProduct).filter(c => c) ).map(c => c.intendedProduct).filter(c => c)
).size; ).size;
document.getElementById('totalCustomers').textContent = totalCustomers; document.getElementById('totalCustomers').textContent = totalCustomers;
document.getElementById('newCustomers').textContent = newCustomers; document.getElementById('newCustomers').textContent = newCustomers;
document.getElementById('totalProducts').textContent = products; document.getElementById('totalProducts').textContent = products;
document.getElementById('completedTasks').textContent = completed; document.getElementById('completedTasks').textContent = completed;
} }
function renderStatusChart(customers) { function renderStatusChart(customers) {
const ctx = document.getElementById('statusChart').getContext('2d'); const ctx = document.getElementById('statusChart').getContext('2d');
const selectedField = document.getElementById('chartFieldSelect').value; const selectedField = document.getElementById('chartFieldSelect').value;
const chartTitle = document.getElementById('statusChartTitle').value || '数据分布'; const chartTitle = document.getElementById('statusChartTitle').value || '数据分布';
const fieldCount = {}; const fieldCount = {};
customers.forEach(customer => { customers.forEach(customer => {
const value = customer[selectedField] || '未设置'; const value = customer[selectedField] || '未设置';
fieldCount[value] = (fieldCount[value] || 0) + 1; fieldCount[value] = (fieldCount[value] || 0) + 1;
}); });
const labels = Object.keys(fieldCount); const labels = Object.keys(fieldCount);
const data = Object.values(fieldCount); const data = Object.values(fieldCount);
if (statusChartInstance) { if (statusChartInstance) {
statusChartInstance.destroy(); statusChartInstance.destroy();
} }
statusChartInstance = new Chart(ctx, { statusChartInstance = new Chart(ctx, {
type: 'pie', type: 'pie',
data: { data: {
@ -940,38 +940,38 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
} }
// Render customer type chart // Render customer type chart
function renderTypeChart(customers) { function renderTypeChart(customers) {
console.log('renderTypeChart called with customers:', customers); console.log('renderTypeChart called with customers:', customers);
const canvas = document.getElementById('typeChart'); const canvas = document.getElementById('typeChart');
console.log('typeChart canvas element:', canvas); console.log('typeChart canvas element:', canvas);
if (!canvas) { if (!canvas) {
console.error('typeChart canvas not found'); console.error('typeChart canvas not found');
return; return;
} }
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const chartTitle = document.getElementById('typeChartTitle').value || '客户类型'; const chartTitle = document.getElementById('typeChartTitle').value || '客户类型';
if (typeChartInstance) { if (typeChartInstance) {
typeChartInstance.destroy(); typeChartInstance.destroy();
} }
const selectedField = document.getElementById('typeChartFieldSelect').value; const selectedField = document.getElementById('typeChartFieldSelect').value;
const typeCount = {}; const typeCount = {};
customers.forEach(customer => { customers.forEach(customer => {
const type = customer[selectedField] || '未设置'; const type = customer[selectedField] || '未设置';
typeCount[type] = (typeCount[type] || 0) + 1; typeCount[type] = (typeCount[type] || 0) + 1;
}); });
const labels = Object.keys(typeCount); const labels = Object.keys(typeCount);
const data = Object.values(typeCount); const data = Object.values(typeCount);
console.log('Type chart labels:', labels); console.log('Type chart labels:', labels);
console.log('Type chart data:', data); console.log('Type chart data:', data);
typeChartInstance = new Chart(ctx, { typeChartInstance = new Chart(ctx, {
type: 'doughnut', type: 'doughnut',
data: { data: {
@ -1005,15 +1005,15 @@ document.addEventListener('DOMContentLoaded', function() {
} }
} }
}); });
console.log('Type chart created successfully'); console.log('Type chart created successfully');
} }
// Initialize the app // Initialize the app
loadCustomers().then(() => { loadCustomers().then(() => {
loadAllCustomers(); loadAllCustomers();
}); });
// Pagination event listeners // Pagination event listeners
document.getElementById('firstPage').addEventListener('click', () => { document.getElementById('firstPage').addEventListener('click', () => {
if (currentPage > 1) { if (currentPage > 1) {
@ -1021,43 +1021,43 @@ document.addEventListener('DOMContentLoaded', function() {
applyCustomerFilter(); applyCustomerFilter();
} }
}); });
document.getElementById('prevPage').addEventListener('click', () => { document.getElementById('prevPage').addEventListener('click', () => {
if (currentPage > 1) { if (currentPage > 1) {
currentPage--; currentPage--;
applyCustomerFilter(); applyCustomerFilter();
} }
}); });
document.getElementById('nextPage').addEventListener('click', () => { document.getElementById('nextPage').addEventListener('click', () => {
if (currentPage < totalPages) { if (currentPage < totalPages) {
currentPage++; currentPage++;
applyCustomerFilter(); applyCustomerFilter();
} }
}); });
document.getElementById('lastPage').addEventListener('click', () => { document.getElementById('lastPage').addEventListener('click', () => {
if (currentPage < totalPages) { if (currentPage < totalPages) {
currentPage = totalPages; currentPage = totalPages;
applyCustomerFilter(); applyCustomerFilter();
} }
}); });
document.getElementById('pageSizeSelect').addEventListener('change', (e) => { document.getElementById('pageSizeSelect').addEventListener('change', (e) => {
pageSize = parseInt(e.target.value); pageSize = parseInt(e.target.value);
currentPage = 1; currentPage = 1;
loadCustomers(); loadCustomers();
}); });
// Chart field select event listener // Chart field select event listener
document.getElementById('chartFieldSelect').addEventListener('change', function() { document.getElementById('chartFieldSelect').addEventListener('change', function () {
const startDate = document.getElementById('startDate').value; const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value; const endDate = document.getElementById('endDate').value;
applyDateFilter(startDate, endDate); applyDateFilter(startDate, endDate);
}); });
// Type chart field select event listener // 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 startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value; const endDate = document.getElementById('endDate').value;
applyDateFilter(startDate, endDate); applyDateFilter(startDate, endDate);
@ -1074,7 +1074,7 @@ document.addEventListener('DOMContentLoaded', function() {
window.location.href = '/static/login.html'; window.location.href = '/static/login.html';
} }
}); });
const headerRight = document.querySelector('.header-right'); const headerRight = document.querySelector('.header-right');
if (headerRight) { if (headerRight) {
headerRight.appendChild(logoutBtn); headerRight.appendChild(logoutBtn);

View File

@ -1,14 +1,14 @@
package storage package storage
import ( import (
"crm-go/models"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"os" "os"
"sync"
"path/filepath" "path/filepath"
"sync"
"time" "time"
"crm-go/models"
) )
type CustomerStorage interface { type CustomerStorage interface {
@ -31,58 +31,72 @@ func NewCustomerStorage(filePath string) CustomerStorage {
storage := &customerStorage{ storage := &customerStorage{
filePath: filePath, filePath: filePath,
} }
// Ensure the directory exists // Ensure the directory exists
dir := os.DirFS(filePath[:len(filePath)-len("/customers.json")]) dir := os.DirFS(filePath[:len(filePath)-len("/customers.json")])
_ = dir // Use the directory to ensure it exists _ = dir // Use the directory to ensure it exists
return storage return storage
} }
func (cs *customerStorage) GetAllCustomers() ([]models.Customer, error) { func (cs *customerStorage) GetAllCustomers() ([]models.Customer, error) {
cs.mutex.RLock() cs.mutex.RLock()
defer cs.mutex.RUnlock() 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) { func (cs *customerStorage) GetCustomerByID(id string) (*models.Customer, error) {
cs.mutex.RLock() cs.mutex.RLock()
defer cs.mutex.RUnlock() defer cs.mutex.RUnlock()
customers, err := cs.LoadCustomers() customers, err := cs.LoadCustomers()
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, customer := range customers { for _, customer := range customers {
if customer.ID == id { if customer.ID == id {
return &customer, nil return &customer, nil
} }
} }
return nil, nil return nil, nil
} }
func (cs *customerStorage) CreateCustomer(customer models.Customer) error { func (cs *customerStorage) CreateCustomer(customer models.Customer) error {
cs.mutex.Lock() cs.mutex.Lock()
defer cs.mutex.Unlock() defer cs.mutex.Unlock()
if customer.ID == "" { if customer.ID == "" {
customer.ID = generateUUID() customer.ID = generateUUID()
} }
if customer.CreatedAt.IsZero() { if customer.CreatedAt.IsZero() {
customer.CreatedAt = time.Now() customer.CreatedAt = time.Now()
} }
customers, err := cs.LoadCustomers() customers, err := cs.LoadCustomers()
if err != nil { if err != nil {
return err return err
} }
customers = append(customers, customer) customers = append(customers, customer)
return cs.SaveCustomers(customers) return cs.SaveCustomers(customers)
} }
@ -91,19 +105,19 @@ func generateUUID() string {
rand.Read(bytes) rand.Read(bytes)
bytes[6] = (bytes[6] & 0x0f) | 0x40 // Version 4 bytes[6] = (bytes[6] & 0x0f) | 0x40 // Version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80 // Variant bytes[8] = (bytes[8] & 0x3f) | 0x80 // Variant
return hex.EncodeToString(bytes) return hex.EncodeToString(bytes)
} }
func (cs *customerStorage) UpdateCustomer(id string, updates models.UpdateCustomerRequest) error { func (cs *customerStorage) UpdateCustomer(id string, updates models.UpdateCustomerRequest) error {
cs.mutex.Lock() cs.mutex.Lock()
defer cs.mutex.Unlock() defer cs.mutex.Unlock()
customers, err := cs.LoadCustomers() customers, err := cs.LoadCustomers()
if err != nil { if err != nil {
return err return err
} }
for i, customer := range customers { for i, customer := range customers {
if customer.ID == id { if customer.ID == id {
if updates.CustomerName != nil { if updates.CustomerName != nil {
@ -133,30 +147,30 @@ func (cs *customerStorage) UpdateCustomer(id string, updates models.UpdateCustom
if updates.Reporter != nil { if updates.Reporter != nil {
customers[i].Reporter = *updates.Reporter customers[i].Reporter = *updates.Reporter
} }
return cs.SaveCustomers(customers) return cs.SaveCustomers(customers)
} }
} }
return nil // Customer not found, but not an error return nil // Customer not found, but not an error
} }
func (cs *customerStorage) DeleteCustomer(id string) error { func (cs *customerStorage) DeleteCustomer(id string) error {
cs.mutex.Lock() cs.mutex.Lock()
defer cs.mutex.Unlock() defer cs.mutex.Unlock()
customers, err := cs.LoadCustomers() customers, err := cs.LoadCustomers()
if err != nil { if err != nil {
return err return err
} }
for i, customer := range customers { for i, customer := range customers {
if customer.ID == id { if customer.ID == id {
customers = append(customers[:i], customers[i+1:]...) customers = append(customers[:i], customers[i+1:]...)
return cs.SaveCustomers(customers) return cs.SaveCustomers(customers)
} }
} }
return nil // Customer not found, but not an error 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 { if err := os.MkdirAll(dir, 0755); err != nil {
return err return err
} }
data, err := json.MarshalIndent(customers, "", " ") data, err := json.MarshalIndent(customers, "", " ")
if err != nil { if err != nil {
return err return err
} }
return os.WriteFile(cs.filePath, data, 0644) 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 empty slice if file doesn't exist
return []models.Customer{}, nil return []models.Customer{}, nil
} }
data, err := os.ReadFile(cs.filePath) data, err := os.ReadFile(cs.filePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var customers []models.Customer var customers []models.Customer
if err := json.Unmarshal(data, &customers); err != nil { if err := json.Unmarshal(data, &customers); err != nil {
return nil, err return nil, err
} }
return customers, nil return customers, nil
} }
func (cs *customerStorage) CustomerExists(customer models.Customer) (bool, error) { func (cs *customerStorage) CustomerExists(customer models.Customer) (bool, error) {
cs.mutex.RLock() cs.mutex.RLock()
defer cs.mutex.RUnlock() defer cs.mutex.RUnlock()
customers, err := cs.LoadCustomers() customers, err := cs.LoadCustomers()
if err != nil { if err != nil {
return false, err return false, err
} }
for _, existingCustomer := range customers { for _, existingCustomer := range customers {
if existingCustomer.Description == customer.Description { if existingCustomer.Description == customer.Description {
return true, nil return true, nil
} }
} }
return false, nil return false, nil
} }