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">
|
<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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user