fix_bug
This commit is contained in:
parent
9952f1569e
commit
6da510f7ea
@ -2163,3 +2163,70 @@ tr:hover .action-cell {
|
|||||||
color: var(--medium-gray);
|
color: var(--medium-gray);
|
||||||
font-family: system-ui;
|
font-family: system-ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Refresh Button Animation */
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.refreshing i {
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.refreshing {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Refresh button success feedback */
|
||||||
|
.icon-btn.refresh-success {
|
||||||
|
color: #27ae60 !important;
|
||||||
|
background-color: rgba(39, 174, 96, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.refresh-success i::before {
|
||||||
|
content: "\f00c";
|
||||||
|
/* Font Awesome check icon */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table refresh animation */
|
||||||
|
@keyframes tableRefresh {
|
||||||
|
0% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-refreshing {
|
||||||
|
animation: tableRefresh 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Row highlight on refresh */
|
||||||
|
@keyframes rowHighlight {
|
||||||
|
0% {
|
||||||
|
background-color: rgba(255, 107, 53, 0.2);
|
||||||
|
transform: translateX(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-color: transparent;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-refreshing tbody tr {
|
||||||
|
animation: rowHighlight 0.4s ease-out;
|
||||||
|
}
|
||||||
@ -54,7 +54,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a href="#" class="nav-item" data-section="customer">
|
<a href="#" class="nav-item" data-section="customer">
|
||||||
<i class="fas fa-user-plus"></i>
|
<i class="fas fa-user-plus"></i>
|
||||||
<span>每周客户进度</span>
|
<span>每周进度</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="nav-item" data-section="followup">
|
<a href="#" class="nav-item" data-section="followup">
|
||||||
<i class="fas fa-tasks"></i>
|
<i class="fas fa-tasks"></i>
|
||||||
@ -127,12 +127,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="icon-btn" title="通知">
|
|
||||||
<i class="fas fa-bell"></i>
|
|
||||||
</button>
|
|
||||||
<button class="icon-btn" title="设置">
|
|
||||||
<i class="fas fa-cog"></i>
|
|
||||||
</button>
|
|
||||||
<button class="icon-btn" id="logoutBtn" title="退出登录">
|
<button class="icon-btn" id="logoutBtn" title="退出登录">
|
||||||
<i class="fas fa-sign-out-alt"></i>
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -843,6 +838,19 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form id="editTrialPeriodForm">
|
<form id="editTrialPeriodForm">
|
||||||
<input type="hidden" id="editTrialPeriodId">
|
<input type="hidden" id="editTrialPeriodId">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>是否试用</label>
|
||||||
|
<div style="display: flex; gap: 20px; align-items: center;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
|
||||||
|
<input type="radio" name="editIsTrial" value="true" checked>
|
||||||
|
<span>是</span>
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
|
||||||
|
<input type="radio" name="editIsTrial" value="false">
|
||||||
|
<span>否</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editTrialStartTime">开始时间</label>
|
<label for="editTrialStartTime">开始时间</label>
|
||||||
<input type="datetime-local" id="editTrialStartTime" name="startTime" required>
|
<input type="datetime-local" id="editTrialStartTime" name="startTime" required>
|
||||||
|
|||||||
@ -412,7 +412,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
currentSection = 'customer';
|
currentSection = 'customer';
|
||||||
loadCustomers();
|
loadCustomers();
|
||||||
} else if (section === 'trialPeriods') {
|
} else if (section === 'trialPeriods') {
|
||||||
console.log('Switching to trial periods section');
|
|
||||||
const trialPeriodsSection = document.getElementById('trialPeriodsSection');
|
const trialPeriodsSection = document.getElementById('trialPeriodsSection');
|
||||||
if (trialPeriodsSection) {
|
if (trialPeriodsSection) {
|
||||||
trialPeriodsSection.classList.add('active');
|
trialPeriodsSection.classList.add('active');
|
||||||
@ -423,9 +422,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
// Load customers first, then trial periods
|
// Load customers first, then trial periods
|
||||||
if (typeof loadCustomersForDropdown === 'function' && typeof loadAllTrialPeriods === 'function') {
|
if (typeof loadCustomersForDropdown === 'function' && typeof loadAllTrialPeriods === 'function') {
|
||||||
console.log('Loading customers first, then trial periods');
|
|
||||||
loadCustomersForDropdown().then(() => {
|
loadCustomersForDropdown().then(() => {
|
||||||
console.log('Customers loaded, now loading trial periods');
|
|
||||||
loadAllTrialPeriods();
|
loadAllTrialPeriods();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -475,9 +472,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
// Populate customer filter dropdown
|
// Populate customer filter dropdown
|
||||||
function populateCustomerFilter() {
|
function populateCustomerFilter() {
|
||||||
console.log('Populating filter, allCustomers:', allCustomers);
|
|
||||||
const uniqueCustomers = [...new Set(allCustomers.map(c => c.customerName).filter(c => c))];
|
const uniqueCustomers = [...new Set(allCustomers.map(c => c.customerName).filter(c => c))];
|
||||||
console.log('Unique customers:', uniqueCustomers);
|
|
||||||
customerFilter.innerHTML = '<option value="">全部客户</option>';
|
customerFilter.innerHTML = '<option value="">全部客户</option>';
|
||||||
|
|
||||||
uniqueCustomers.forEach(customer => {
|
uniqueCustomers.forEach(customer => {
|
||||||
@ -643,9 +638,27 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (refreshCustomersBtn) {
|
if (refreshCustomersBtn) {
|
||||||
refreshCustomersBtn.addEventListener('click', () => {
|
refreshCustomersBtn.addEventListener('click', async () => {
|
||||||
if (currentSection === 'customer') {
|
// Add refreshing animation
|
||||||
loadCustomers();
|
refreshCustomersBtn.classList.add('refreshing');
|
||||||
|
const table = document.getElementById('customerTable');
|
||||||
|
try {
|
||||||
|
await loadCustomers();
|
||||||
|
// Show success feedback briefly
|
||||||
|
refreshCustomersBtn.classList.remove('refreshing');
|
||||||
|
refreshCustomersBtn.classList.add('refresh-success');
|
||||||
|
// Add table refresh animation
|
||||||
|
if (table) {
|
||||||
|
table.classList.add('table-refreshing');
|
||||||
|
setTimeout(() => {
|
||||||
|
table.classList.remove('table-refreshing');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshCustomersBtn.classList.remove('refresh-success');
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
refreshCustomersBtn.classList.remove('refreshing');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -672,16 +685,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
// Load all customers for dashboard
|
// Load all customers for dashboard
|
||||||
async function loadAllCustomers() {
|
async function loadAllCustomers() {
|
||||||
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);
|
|
||||||
|
|
||||||
if (data.customers) {
|
if (data.customers) {
|
||||||
dashboardCustomers = data.customers;
|
dashboardCustomers = data.customers;
|
||||||
console.log('Dashboard customers set:', dashboardCustomers.length, 'customers');
|
|
||||||
updateDashboardStats(dashboardCustomers);
|
updateDashboardStats(dashboardCustomers);
|
||||||
renderStatusChart(dashboardCustomers);
|
renderStatusChart(dashboardCustomers);
|
||||||
renderTypeChart(dashboardCustomers);
|
renderTypeChart(dashboardCustomers);
|
||||||
@ -1070,7 +1079,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
// Dashboard functionality
|
// Dashboard functionality
|
||||||
async function loadDashboardData() {
|
async function loadDashboardData() {
|
||||||
console.log('loadDashboardData called');
|
|
||||||
await loadAllCustomers();
|
await loadAllCustomers();
|
||||||
await loadTrialCustomersForDashboard();
|
await loadTrialCustomersForDashboard();
|
||||||
await loadFollowUpCount();
|
await loadFollowUpCount();
|
||||||
@ -1264,9 +1272,7 @@ 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);
|
|
||||||
const canvas = document.getElementById('typeChart');
|
const canvas = document.getElementById('typeChart');
|
||||||
console.log('typeChart canvas element:', canvas);
|
|
||||||
|
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
console.error('typeChart canvas not found');
|
console.error('typeChart canvas not found');
|
||||||
@ -1290,9 +1296,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
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 data:', data);
|
|
||||||
|
|
||||||
typeChartInstance = new Chart(ctx, {
|
typeChartInstance = new Chart(ctx, {
|
||||||
type: 'doughnut',
|
type: 'doughnut',
|
||||||
data: {
|
data: {
|
||||||
@ -1367,8 +1370,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Type chart created successfully');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render trend line chart
|
// Render trend line chart
|
||||||
@ -1585,8 +1586,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Trend chart created successfully');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the app
|
// Initialize the app
|
||||||
@ -1931,8 +1930,28 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
// Refresh follow-ups button
|
// Refresh follow-ups button
|
||||||
if (refreshFollowupsBtn) {
|
if (refreshFollowupsBtn) {
|
||||||
refreshFollowupsBtn.addEventListener('click', () => {
|
refreshFollowupsBtn.addEventListener('click', async () => {
|
||||||
loadFollowUps();
|
// Add refreshing animation
|
||||||
|
refreshFollowupsBtn.classList.add('refreshing');
|
||||||
|
const table = document.getElementById('followupTable');
|
||||||
|
try {
|
||||||
|
await loadFollowUps();
|
||||||
|
// Show success feedback briefly
|
||||||
|
refreshFollowupsBtn.classList.remove('refreshing');
|
||||||
|
refreshFollowupsBtn.classList.add('refresh-success');
|
||||||
|
// Add table refresh animation
|
||||||
|
if (table) {
|
||||||
|
table.classList.add('table-refreshing');
|
||||||
|
setTimeout(() => {
|
||||||
|
table.classList.remove('table-refreshing');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshFollowupsBtn.classList.remove('refresh-success');
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
refreshFollowupsBtn.classList.remove('refreshing');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2175,9 +2194,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (headerLogoutBtn) {
|
if (headerLogoutBtn) {
|
||||||
headerLogoutBtn.addEventListener('click', () => {
|
headerLogoutBtn.addEventListener('click', () => {
|
||||||
if (confirm('确定要退出登录吗?')) {
|
if (confirm('确定要退出登录吗?')) {
|
||||||
localStorage.removeItem('authToken');
|
localStorage.removeItem('crmToken');
|
||||||
localStorage.removeItem('crmSearchHistory');
|
localStorage.removeItem('crmSearchHistory');
|
||||||
window.location.reload();
|
window.location.href = '/static/login.html';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
225
frontend/js/tests.js
Normal file
225
frontend/js/tests.js
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
// Test suite for CRM Frontend JavaScript
|
||||||
|
// Run with: open this file in a browser or use a test runner like Jest
|
||||||
|
|
||||||
|
// Simple test framework
|
||||||
|
const TestRunner = {
|
||||||
|
tests: [],
|
||||||
|
results: [],
|
||||||
|
|
||||||
|
add(name, testFn) {
|
||||||
|
this.tests.push({ name, testFn });
|
||||||
|
},
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
console.log('🧪 Running CRM Frontend Tests...\n');
|
||||||
|
this.results = [];
|
||||||
|
|
||||||
|
for (const test of this.tests) {
|
||||||
|
try {
|
||||||
|
await test.testFn();
|
||||||
|
this.results.push({ name: test.name, passed: true });
|
||||||
|
console.log(`✅ ${test.name}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.results.push({ name: test.name, passed: false, error: error.message });
|
||||||
|
console.log(`❌ ${test.name}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const passed = this.results.filter(r => r.passed).length;
|
||||||
|
const total = this.results.length;
|
||||||
|
console.log(`\n📊 Results: ${passed}/${total} tests passed`);
|
||||||
|
|
||||||
|
return this.results;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function assert(condition, message) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(message || 'Assertion failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertEqual(actual, expected, message) {
|
||||||
|
if (actual !== expected) {
|
||||||
|
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Test: Sidebar menu text is "每周进度"
|
||||||
|
// ==========================================
|
||||||
|
TestRunner.add('Sidebar menu shows "每周进度" (not "每周客户进度")', () => {
|
||||||
|
const navItems = document.querySelectorAll('.nav-item');
|
||||||
|
let found = false;
|
||||||
|
let hasOldText = false;
|
||||||
|
|
||||||
|
navItems.forEach(item => {
|
||||||
|
const text = item.textContent.trim();
|
||||||
|
if (text.includes('每周进度') && !text.includes('每周客户进度')) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
if (text.includes('每周客户进度')) {
|
||||||
|
hasOldText = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(!hasOldText, 'Found old text "每周客户进度" which should be removed');
|
||||||
|
assert(found, 'Could not find "每周进度" in sidebar');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Test: Bell and Settings buttons removed from header
|
||||||
|
// ==========================================
|
||||||
|
TestRunner.add('Header does not have bell and settings icons', () => {
|
||||||
|
const headerRight = document.querySelector('.header-right');
|
||||||
|
|
||||||
|
if (headerRight) {
|
||||||
|
const bellBtn = headerRight.querySelector('.fa-bell');
|
||||||
|
const cogBtn = headerRight.querySelector('.fa-cog');
|
||||||
|
|
||||||
|
assert(!bellBtn, 'Bell icon should be removed from header');
|
||||||
|
assert(!cogBtn, 'Settings (cog) icon should be removed from header');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify logout button still exists
|
||||||
|
const logoutBtn = document.getElementById('logoutBtn');
|
||||||
|
assert(logoutBtn, 'Logout button should exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Test: Add trial modal has "是否试用" option
|
||||||
|
// ==========================================
|
||||||
|
TestRunner.add('Add trial modal has "是否试用" radio options', () => {
|
||||||
|
const modal = document.getElementById('addTrialPeriodModal');
|
||||||
|
assert(modal, 'Add trial period modal should exist');
|
||||||
|
|
||||||
|
const isTrialRadios = modal.querySelectorAll('input[name="isTrial"]');
|
||||||
|
assert(isTrialRadios.length === 2, `Should have 2 radio buttons for "是否试用", found ${isTrialRadios.length}`);
|
||||||
|
|
||||||
|
const trueRadio = modal.querySelector('input[name="isTrial"][value="true"]');
|
||||||
|
const falseRadio = modal.querySelector('input[name="isTrial"][value="false"]');
|
||||||
|
|
||||||
|
assert(trueRadio, 'Should have a radio for isTrial=true');
|
||||||
|
assert(falseRadio, 'Should have a radio for isTrial=false');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Test: Edit trial modal has "是否试用" option
|
||||||
|
// ==========================================
|
||||||
|
TestRunner.add('Edit trial modal has "是否试用" radio options', () => {
|
||||||
|
const modal = document.getElementById('editTrialPeriodModal');
|
||||||
|
assert(modal, 'Edit trial period modal should exist');
|
||||||
|
|
||||||
|
const editIsTrialRadios = modal.querySelectorAll('input[name="editIsTrial"]');
|
||||||
|
assert(editIsTrialRadios.length === 2, `Should have 2 radio buttons for edit "是否试用", found ${editIsTrialRadios.length}`);
|
||||||
|
|
||||||
|
const trueRadio = modal.querySelector('input[name="editIsTrial"][value="true"]');
|
||||||
|
const falseRadio = modal.querySelector('input[name="editIsTrial"][value="false"]');
|
||||||
|
|
||||||
|
assert(trueRadio, 'Should have a radio for editIsTrial=true');
|
||||||
|
assert(falseRadio, 'Should have a radio for editIsTrial=false');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Test: Trial periods table has "是否试用" column
|
||||||
|
// ==========================================
|
||||||
|
TestRunner.add('Trial periods table has "是否试用" column header', () => {
|
||||||
|
const trialPeriodsSection = document.getElementById('trialPeriodsSection');
|
||||||
|
if (trialPeriodsSection) {
|
||||||
|
const headers = trialPeriodsSection.querySelectorAll('th');
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
headers.forEach(header => {
|
||||||
|
if (header.textContent.includes('是否试用')) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(found, 'Trial periods table should have "是否试用" column header');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Test: Logout button clears crmToken
|
||||||
|
// ==========================================
|
||||||
|
TestRunner.add('Logout button has correct event listener setup', () => {
|
||||||
|
const logoutBtn = document.getElementById('logoutBtn');
|
||||||
|
assert(logoutBtn, 'Logout button should exist');
|
||||||
|
|
||||||
|
// Check that the button is wired up (we can't directly test the function without clicking)
|
||||||
|
assert(logoutBtn.id === 'logoutBtn', 'Logout button should have correct ID');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Test: Refresh button for customers exists
|
||||||
|
// ==========================================
|
||||||
|
TestRunner.add('Refresh customers button exists', () => {
|
||||||
|
const refreshBtn = document.getElementById('refreshCustomersBtn');
|
||||||
|
assert(refreshBtn, 'Refresh customers button should exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Test: Refresh button for followups exists
|
||||||
|
// ==========================================
|
||||||
|
TestRunner.add('Refresh followups button exists', () => {
|
||||||
|
const refreshBtn = document.getElementById('refreshFollowupsBtn');
|
||||||
|
assert(refreshBtn, 'Refresh followups button should exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Test: Add trial button exists
|
||||||
|
// ==========================================
|
||||||
|
TestRunner.add('Add trial button exists', () => {
|
||||||
|
const addTrialBtn = document.getElementById('addTrialBtn');
|
||||||
|
assert(addTrialBtn, 'Add trial button should exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Test: Quick add dropdown exists
|
||||||
|
// ==========================================
|
||||||
|
TestRunner.add('Quick add dropdown exists in header', () => {
|
||||||
|
const quickAddBtn = document.getElementById('headerQuickAddBtn');
|
||||||
|
const quickAddDropdown = document.getElementById('headerQuickAddDropdown');
|
||||||
|
|
||||||
|
assert(quickAddBtn, 'Quick add button should exist');
|
||||||
|
assert(quickAddDropdown, 'Quick add dropdown should exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Test: Global search input exists
|
||||||
|
// ==========================================
|
||||||
|
TestRunner.add('Global search input exists', () => {
|
||||||
|
const searchInput = document.getElementById('globalSearchInput');
|
||||||
|
assert(searchInput, 'Global search input should exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Test: All required sections exist
|
||||||
|
// ==========================================
|
||||||
|
TestRunner.add('All required sections exist', () => {
|
||||||
|
const customerSection = document.getElementById('customerSection');
|
||||||
|
const dashboardSection = document.getElementById('dashboardSection');
|
||||||
|
const followupSection = document.getElementById('followupSection');
|
||||||
|
const trialPeriodsSection = document.getElementById('trialPeriodsSection');
|
||||||
|
|
||||||
|
assert(customerSection, 'Customer section should exist');
|
||||||
|
assert(dashboardSection, 'Dashboard section should exist');
|
||||||
|
assert(followupSection, 'Followup section should exist');
|
||||||
|
assert(trialPeriodsSection, 'Trial periods section should exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Run tests when DOM is loaded
|
||||||
|
// ==========================================
|
||||||
|
if (typeof document !== 'undefined' && document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
setTimeout(() => TestRunner.run(), 1000);
|
||||||
|
});
|
||||||
|
} else if (typeof document !== 'undefined') {
|
||||||
|
setTimeout(() => TestRunner.run(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for use in other modules
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = { TestRunner, assert, assertEqual };
|
||||||
|
}
|
||||||
@ -83,7 +83,6 @@ function initTrialPeriodsPage() {
|
|||||||
// Load customers map for displaying customer names in table
|
// Load customers map for displaying customer names in table
|
||||||
async function loadCustomersMap() {
|
async function loadCustomersMap() {
|
||||||
try {
|
try {
|
||||||
console.log('Loading customers map...');
|
|
||||||
const response = await authenticatedFetch('/api/customers/list');
|
const response = await authenticatedFetch('/api/customers/list');
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
@ -92,12 +91,9 @@ async function loadCustomersMap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Customers data:', data);
|
|
||||||
|
|
||||||
// Use the complete customer map for ID->Name lookups
|
// Use the complete customer map for ID->Name lookups
|
||||||
customersMap = data.customerMap || {};
|
customersMap = data.customerMap || {};
|
||||||
|
|
||||||
console.log('Number of customer ID mappings:', Object.keys(customersMap).length);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading customers map:', error);
|
console.error('Error loading customers map:', error);
|
||||||
}
|
}
|
||||||
@ -374,7 +370,6 @@ function updateTrialPagination() {
|
|||||||
|
|
||||||
// Open add trial modal
|
// Open add trial modal
|
||||||
function openAddTrialModal() {
|
function openAddTrialModal() {
|
||||||
console.log('Opening add trial modal');
|
|
||||||
document.getElementById('trialCustomerInput').value = '';
|
document.getElementById('trialCustomerInput').value = '';
|
||||||
document.querySelector('input[name="isTrial"][value="true"]').checked = true;
|
document.querySelector('input[name="isTrial"][value="true"]').checked = true;
|
||||||
document.getElementById('trialStartTime').value = '';
|
document.getElementById('trialStartTime').value = '';
|
||||||
@ -395,6 +390,13 @@ function openEditTrialModal(periodId) {
|
|||||||
document.getElementById('editTrialStartTime').value = formatDateTimeLocal(startDate);
|
document.getElementById('editTrialStartTime').value = formatDateTimeLocal(startDate);
|
||||||
document.getElementById('editTrialEndTime').value = formatDateTimeLocal(endDate);
|
document.getElementById('editTrialEndTime').value = formatDateTimeLocal(endDate);
|
||||||
|
|
||||||
|
// Set isTrial radio button
|
||||||
|
const isTrial = (period.isTrial !== undefined && period.isTrial !== null) ? period.isTrial : true;
|
||||||
|
const isTrialRadio = document.querySelector(`input[name="editIsTrial"][value="${isTrial}"]`);
|
||||||
|
if (isTrialRadio) {
|
||||||
|
isTrialRadio.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('editTrialPeriodModal').style.display = 'block';
|
document.getElementById('editTrialPeriodModal').style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -232,6 +232,10 @@ async function updateTrialPeriod() {
|
|||||||
const startTime = document.getElementById('editTrialStartTime').value;
|
const startTime = document.getElementById('editTrialStartTime').value;
|
||||||
const endTime = document.getElementById('editTrialEndTime').value;
|
const endTime = document.getElementById('editTrialEndTime').value;
|
||||||
|
|
||||||
|
// Get isTrial value from radio buttons
|
||||||
|
const isTrialRadio = document.querySelector('input[name="editIsTrial"]:checked');
|
||||||
|
const isTrial = isTrialRadio ? isTrialRadio.value === 'true' : true;
|
||||||
|
|
||||||
if (!startTime || !endTime) {
|
if (!startTime || !endTime) {
|
||||||
alert('请填写开始时间和结束时间');
|
alert('请填写开始时间和结束时间');
|
||||||
return;
|
return;
|
||||||
@ -239,7 +243,8 @@ async function updateTrialPeriod() {
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
startTime: new Date(startTime).toISOString(),
|
startTime: new Date(startTime).toISOString(),
|
||||||
endTime: new Date(endTime).toISOString()
|
endTime: new Date(endTime).toISOString(),
|
||||||
|
isTrial: isTrial
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -253,7 +258,14 @@ async function updateTrialPeriod() {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
document.getElementById('editTrialPeriodModal').style.display = 'none';
|
document.getElementById('editTrialPeriodModal').style.display = 'none';
|
||||||
await loadTrialPeriods(currentCustomerId);
|
// Load trial periods for specific customer (for embedded list)
|
||||||
|
if (currentCustomerId) {
|
||||||
|
await loadTrialPeriods(currentCustomerId);
|
||||||
|
}
|
||||||
|
// Also refresh the main trial periods page list
|
||||||
|
if (typeof loadAllTrialPeriods === 'function') {
|
||||||
|
await loadAllTrialPeriods();
|
||||||
|
}
|
||||||
alert('试用时间更新成功!');
|
alert('试用时间更新成功!');
|
||||||
} else {
|
} else {
|
||||||
alert('更新试用时间时出错');
|
alert('更新试用时间时出错');
|
||||||
|
|||||||
@ -44,7 +44,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户名和密码
|
// 验证用户名和密码
|
||||||
if req.Username != "admin" || req.Password != "admin123" {
|
if req.Username != "admin" || req.Password != "digua666" {
|
||||||
http.Error(w, "用户名或密码错误", http.StatusUnauthorized)
|
http.Error(w, "用户名或密码错误", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -148,6 +148,9 @@ func (ts *trialPeriodStorage) UpdateTrialPeriod(id string, updates models.Update
|
|||||||
trialPeriods[i].EndTime = endTime
|
trialPeriods[i].EndTime = endTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if updates.IsTrial != nil {
|
||||||
|
trialPeriods[i].IsTrial = *updates.IsTrial
|
||||||
|
}
|
||||||
|
|
||||||
return ts.saveTrialPeriods(trialPeriods)
|
return ts.saveTrialPeriods(trialPeriods)
|
||||||
}
|
}
|
||||||
|
|||||||
272
internal/storage/trial_period_storage_test.go
Normal file
272
internal/storage/trial_period_storage_test.go
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"crm-go/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdateTrialPeriodWithIsTrial(t *testing.T) {
|
||||||
|
// Create a temporary directory for test data
|
||||||
|
tempDir, err := os.MkdirTemp("", "trial_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create a test storage
|
||||||
|
testFilePath := filepath.Join(tempDir, "trial_periods.json")
|
||||||
|
storage := NewTrialPeriodStorage(testFilePath)
|
||||||
|
|
||||||
|
// Create a trial period with isTrial = true
|
||||||
|
now := time.Now()
|
||||||
|
startTime := now
|
||||||
|
endTime := now.Add(7 * 24 * time.Hour)
|
||||||
|
|
||||||
|
trialPeriod := models.TrialPeriod{
|
||||||
|
CustomerID: "test-customer-001",
|
||||||
|
StartTime: startTime,
|
||||||
|
EndTime: endTime,
|
||||||
|
IsTrial: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
createdPeriod, err := storage.CreateTrialPeriod(trialPeriod)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create trial period: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if createdPeriod.ID == "" {
|
||||||
|
t.Fatal("Created trial period should have an ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !createdPeriod.IsTrial {
|
||||||
|
t.Fatal("Created trial period should have IsTrial = true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test updating isTrial to false
|
||||||
|
isTrialFalse := false
|
||||||
|
updateRequest := models.UpdateTrialPeriodRequest{
|
||||||
|
IsTrial: &isTrialFalse,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = storage.UpdateTrialPeriod(createdPeriod.ID, updateRequest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to update trial period: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the update
|
||||||
|
updatedPeriod, err := storage.GetTrialPeriodByID(createdPeriod.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get updated trial period: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatedPeriod == nil {
|
||||||
|
t.Fatal("Updated trial period should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatedPeriod.IsTrial {
|
||||||
|
t.Fatal("Updated trial period should have IsTrial = false")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test updating isTrial back to true
|
||||||
|
isTrialTrue := true
|
||||||
|
updateRequest2 := models.UpdateTrialPeriodRequest{
|
||||||
|
IsTrial: &isTrialTrue,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = storage.UpdateTrialPeriod(createdPeriod.ID, updateRequest2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to update trial period back to true: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the update
|
||||||
|
updatedPeriod2, err := storage.GetTrialPeriodByID(createdPeriod.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get updated trial period: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updatedPeriod2.IsTrial {
|
||||||
|
t.Fatal("Updated trial period should have IsTrial = true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateTrialPeriodWithStartEndTime(t *testing.T) {
|
||||||
|
// Create a temporary directory for test data
|
||||||
|
tempDir, err := os.MkdirTemp("", "trial_test_time")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create a test storage
|
||||||
|
testFilePath := filepath.Join(tempDir, "trial_periods.json")
|
||||||
|
storage := NewTrialPeriodStorage(testFilePath)
|
||||||
|
|
||||||
|
// Create a trial period
|
||||||
|
now := time.Now()
|
||||||
|
startTime := now
|
||||||
|
endTime := now.Add(7 * 24 * time.Hour)
|
||||||
|
|
||||||
|
trialPeriod := models.TrialPeriod{
|
||||||
|
CustomerID: "test-customer-002",
|
||||||
|
StartTime: startTime,
|
||||||
|
EndTime: endTime,
|
||||||
|
IsTrial: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
createdPeriod, err := storage.CreateTrialPeriod(trialPeriod)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create trial period: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update start and end times along with isTrial
|
||||||
|
newStartTime := now.Add(1 * 24 * time.Hour).Format(time.RFC3339)
|
||||||
|
newEndTime := now.Add(14 * 24 * time.Hour).Format(time.RFC3339)
|
||||||
|
isTrialFalse := false
|
||||||
|
|
||||||
|
updateRequest := models.UpdateTrialPeriodRequest{
|
||||||
|
StartTime: &newStartTime,
|
||||||
|
EndTime: &newEndTime,
|
||||||
|
IsTrial: &isTrialFalse,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = storage.UpdateTrialPeriod(createdPeriod.ID, updateRequest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to update trial period: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all updates
|
||||||
|
updatedPeriod, err := storage.GetTrialPeriodByID(createdPeriod.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get updated trial period: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatedPeriod == nil {
|
||||||
|
t.Fatal("Updated trial period should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatedPeriod.IsTrial {
|
||||||
|
t.Fatal("Updated trial period should have IsTrial = false")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify start time was updated (approximately)
|
||||||
|
expectedStartTime, _ := time.Parse(time.RFC3339, newStartTime)
|
||||||
|
if !updatedPeriod.StartTime.Equal(expectedStartTime) {
|
||||||
|
t.Fatalf("StartTime mismatch: expected %v, got %v", expectedStartTime, updatedPeriod.StartTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify end time was updated
|
||||||
|
expectedEndTime, _ := time.Parse(time.RFC3339, newEndTime)
|
||||||
|
if !updatedPeriod.EndTime.Equal(expectedEndTime) {
|
||||||
|
t.Fatalf("EndTime mismatch: expected %v, got %v", expectedEndTime, updatedPeriod.EndTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAllTrialPeriods(t *testing.T) {
|
||||||
|
// Create a temporary directory for test data
|
||||||
|
tempDir, err := os.MkdirTemp("", "trial_test_all")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create a test storage
|
||||||
|
testFilePath := filepath.Join(tempDir, "trial_periods.json")
|
||||||
|
storage := NewTrialPeriodStorage(testFilePath)
|
||||||
|
|
||||||
|
// Initially should be empty
|
||||||
|
periods, err := storage.GetAllTrialPeriods()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get all trial periods: %v", err)
|
||||||
|
}
|
||||||
|
if len(periods) != 0 {
|
||||||
|
t.Fatalf("Expected 0 periods, got %d", len(periods))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create multiple trial periods
|
||||||
|
now := time.Now()
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
trialPeriod := models.TrialPeriod{
|
||||||
|
CustomerID: "test-customer-bulk",
|
||||||
|
StartTime: now.Add(time.Duration(i) * 24 * time.Hour),
|
||||||
|
EndTime: now.Add(time.Duration(i+7) * 24 * time.Hour),
|
||||||
|
IsTrial: i%2 == 0, // Alternate true/false
|
||||||
|
}
|
||||||
|
_, err := storage.CreateTrialPeriod(trialPeriod)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create trial period %d: %v", i, err)
|
||||||
|
}
|
||||||
|
// Add small delay to ensure different createdAt times
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all periods are retrieved
|
||||||
|
periods, err = storage.GetAllTrialPeriods()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get all trial periods: %v", err)
|
||||||
|
}
|
||||||
|
if len(periods) != 3 {
|
||||||
|
t.Fatalf("Expected 3 periods, got %d", len(periods))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify periods are sorted by CreatedAt descending (newest first)
|
||||||
|
for i := 0; i < len(periods)-1; i++ {
|
||||||
|
if periods[i].CreatedAt.Before(periods[i+1].CreatedAt) {
|
||||||
|
t.Fatal("Periods should be sorted by CreatedAt descending")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteTrialPeriod(t *testing.T) {
|
||||||
|
// Create a temporary directory for test data
|
||||||
|
tempDir, err := os.MkdirTemp("", "trial_test_delete")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create a test storage
|
||||||
|
testFilePath := filepath.Join(tempDir, "trial_periods.json")
|
||||||
|
storage := NewTrialPeriodStorage(testFilePath)
|
||||||
|
|
||||||
|
// Create a trial period
|
||||||
|
now := time.Now()
|
||||||
|
trialPeriod := models.TrialPeriod{
|
||||||
|
CustomerID: "test-customer-delete",
|
||||||
|
StartTime: now,
|
||||||
|
EndTime: now.Add(7 * 24 * time.Hour),
|
||||||
|
IsTrial: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
createdPeriod, err := storage.CreateTrialPeriod(trialPeriod)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create trial period: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it exists
|
||||||
|
existingPeriod, err := storage.GetTrialPeriodByID(createdPeriod.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get trial period: %v", err)
|
||||||
|
}
|
||||||
|
if existingPeriod == nil {
|
||||||
|
t.Fatal("Trial period should exist after creation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the trial period
|
||||||
|
err = storage.DeleteTrialPeriod(createdPeriod.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete trial period: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it no longer exists
|
||||||
|
deletedPeriod, err := storage.GetTrialPeriodByID(createdPeriod.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get trial period after delete: %v", err)
|
||||||
|
}
|
||||||
|
if deletedPeriod != nil {
|
||||||
|
t.Fatal("Trial period should not exist after deletion")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,4 +24,5 @@ type CreateTrialPeriodRequest struct {
|
|||||||
type UpdateTrialPeriodRequest struct {
|
type UpdateTrialPeriodRequest struct {
|
||||||
StartTime *string `json:"startTime,omitempty"`
|
StartTime *string `json:"startTime,omitempty"`
|
||||||
EndTime *string `json:"endTime,omitempty"`
|
EndTime *string `json:"endTime,omitempty"`
|
||||||
|
IsTrial *bool `json:"isTrial,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user