crm/frontend/js/trial-periods-page.js
2026-01-15 21:14:12 +08:00

499 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Trial Periods Page Management
// This file handles the standalone trial periods page
let trialPeriodsData = [];
let filteredTrialPeriodsData = [];
let trialCurrentPage = 1;
let trialPageSize = 10;
let trialTotalItems = 0;
let trialTotalPages = 0;
let customersMap = {}; // Map of customer ID to customer name
let trialStartDateFilter = '';
let trialEndDateFilter = '';
let trialSortOrder = 'desc';
// Initialize trial periods page
function initTrialPeriodsPage() {
const addTrialBtn = document.getElementById('addTrialBtn');
const trialPeriodsSection = document.getElementById('trialPeriodsSection');
if (!trialPeriodsSection) return;
// Add trial button click
if (addTrialBtn) {
addTrialBtn.addEventListener('click', function () {
openAddTrialModal();
});
}
// Pagination controls
document.getElementById('trialFirstPage')?.addEventListener('click', () => {
trialCurrentPage = 1;
applyTrialFiltersAndSort();
});
document.getElementById('trialPrevPage')?.addEventListener('click', () => {
if (trialCurrentPage > 1) {
trialCurrentPage--;
applyTrialFiltersAndSort();
}
});
document.getElementById('trialNextPage')?.addEventListener('click', () => {
if (trialCurrentPage < trialTotalPages) {
trialCurrentPage++;
applyTrialFiltersAndSort();
}
});
document.getElementById('trialLastPage')?.addEventListener('click', () => {
trialCurrentPage = trialTotalPages;
applyTrialFiltersAndSort();
});
document.getElementById('trialPageSizeSelect')?.addEventListener('change', function () {
trialPageSize = parseInt(this.value);
trialCurrentPage = 1;
applyTrialFiltersAndSort();
});
// Filter and sort event listeners
document.getElementById('trialStartDateFilter')?.addEventListener('change', function () {
trialStartDateFilter = this.value;
trialCurrentPage = 1;
applyTrialFiltersAndSort();
});
document.getElementById('trialEndDateFilter')?.addEventListener('change', function () {
trialEndDateFilter = this.value;
trialCurrentPage = 1;
applyTrialFiltersAndSort();
});
document.getElementById('trialSortOrder')?.addEventListener('change', function () {
trialSortOrder = this.value;
trialCurrentPage = 1;
applyTrialFiltersAndSort();
});
// Load customers map for displaying customer names
loadCustomersMap();
}
// Load customers map for displaying customer names in table
async function loadCustomersMap() {
try {
console.log('Loading customers map...');
const response = await authenticatedFetch('/api/customers/list');
if (!response) {
console.error('No response from API');
return;
}
const data = await response.json();
console.log('Customers data:', data);
// Use the complete customer map for ID->Name lookups
customersMap = data.customerMap || {};
console.log('Number of customer ID mappings:', Object.keys(customersMap).length);
} catch (error) {
console.error('Error loading customers map:', error);
}
}
// Alias for loadCustomersMap - used by main.js when switching to trial periods section
async function loadCustomersForDropdown() {
return await loadCustomersMap();
}
// Load all trial periods
async function loadAllTrialPeriods() {
try {
const response = await authenticatedFetch('/api/trial-periods/all');
const data = await response.json();
trialPeriodsData = data.trialPeriods || [];
applyTrialFiltersAndSort();
renderExpiryWarnings();
} catch (error) {
console.error('Error loading trial periods:', error);
trialPeriodsData = [];
filteredTrialPeriodsData = [];
renderTrialPeriodsTable();
}
}
// Apply filters and sorting to trial periods
function applyTrialFiltersAndSort() {
let filtered = [...trialPeriodsData];
// Apply date range filter
if (trialStartDateFilter) {
filtered = filtered.filter(period => {
const endDate = new Date(period.endTime).toISOString().split('T')[0];
return endDate >= trialStartDateFilter;
});
}
if (trialEndDateFilter) {
filtered = filtered.filter(period => {
const endDate = new Date(period.endTime).toISOString().split('T')[0];
return endDate <= trialEndDateFilter;
});
}
// Sort by end time
filtered.sort((a, b) => {
const dateA = new Date(a.endTime);
const dateB = new Date(b.endTime);
return trialSortOrder === 'asc' ? dateA - dateB : dateB - dateA;
});
filteredTrialPeriodsData = filtered;
trialTotalItems = filteredTrialPeriodsData.length;
trialTotalPages = Math.ceil(trialTotalItems / trialPageSize);
renderTrialPeriodsTable();
updateTrialPagination();
}
// Render expiry warning cards
function renderExpiryWarnings() {
const warningsContainer = document.getElementById('trialExpiryWarnings');
if (!warningsContainer) return;
warningsContainer.innerHTML = '';
const now = new Date();
now.setHours(0, 0, 0, 0); // Set to start of today
const warnings = [];
trialPeriodsData.forEach(period => {
const endTime = new Date(period.endTime);
endTime.setHours(0, 0, 0, 0); // Set to start of day
const daysUntilExpiry = Math.ceil((endTime - now) / (1000 * 60 * 60 * 24));
const customerName = customersMap[period.customerId] || period.customerId;
if (daysUntilExpiry >= 0 && daysUntilExpiry <= 3) {
warnings.push({
customerName,
endTime: period.endTime,
daysUntilExpiry,
period
});
}
});
// Sort by days until expiry (most urgent first)
warnings.sort((a, b) => a.daysUntilExpiry - b.daysUntilExpiry);
warnings.forEach(warning => {
const card = document.createElement('div');
let warningClass = 'warning-soon';
let iconClass = 'fa-info-circle';
let title = '试用即将到期';
let message = '';
if (warning.daysUntilExpiry === 0) {
warningClass = 'warning-today';
iconClass = 'fa-exclamation-triangle';
title = '试用今日到期';
message = `<strong>${warning.customerName}</strong> 客户的试用期将于<span class="expiry-warning-time">今天</span>到期,请及时跟进!`;
} else if (warning.daysUntilExpiry === 1) {
warningClass = 'warning-tomorrow';
iconClass = 'fa-exclamation-circle';
title = '试用明日到期';
message = `<strong>${warning.customerName}</strong> 客户的试用期将于<span class="expiry-warning-time">明天</span>到期,请及时跟进!`;
} else {
warningClass = 'warning-soon';
iconClass = 'fa-info-circle';
title = '试用即将到期';
message = `<strong>${warning.customerName}</strong> 客户的试用期将于<span class="expiry-warning-time">${warning.daysUntilExpiry}天后</span>到期,请及时跟进!`;
}
const formattedEndTime = formatDateTime(warning.endTime);
message += `<br><small>试用结束时间:${formattedEndTime}</small>`;
card.className = `expiry-warning-card ${warningClass}`;
card.innerHTML = `
<div class="expiry-warning-icon">
<i class="fas ${iconClass}"></i>
</div>
<div class="expiry-warning-content">
<div class="expiry-warning-title">${title}</div>
<div class="expiry-warning-message">${message}</div>
</div>
<button class="expiry-warning-close" onclick="this.parentElement.remove()">
<i class="fas fa-times"></i>
</button>
`;
warningsContainer.appendChild(card);
});
}
// Render trial periods table
function renderTrialPeriodsTable() {
const tbody = document.getElementById('trialPeriodsBody');
if (!tbody) return;
tbody.innerHTML = '';
if (filteredTrialPeriodsData.length === 0) {
const row = document.createElement('tr');
row.innerHTML = `
<td colspan="6" class="empty-state">
<i class="fas fa-users"></i>
<h3>👥 还没有客户试用信息</h3>
<p>点击上方「添加试用时间」开始管理客户试用</p>
</td>
`;
tbody.appendChild(row);
return;
}
// Paginate data
const startIndex = (trialCurrentPage - 1) * trialPageSize;
const endIndex = Math.min(startIndex + trialPageSize, filteredTrialPeriodsData.length);
const pageData = filteredTrialPeriodsData.slice(startIndex, endIndex);
pageData.forEach(period => {
const row = document.createElement('tr');
row.classList.add('trial-row');
// 显示客户名称如果找不到则显示ID
const customerName = customersMap[period.customerId] || period.customerId;
// 计算状态和到期天数
const now = new Date();
const endDate = new Date(period.endTime);
const startDate = new Date(period.startTime);
const daysUntilExpiry = Math.ceil((endDate - now) / (1000 * 60 * 60 * 24));
const isTrial = (period.isTrial !== undefined && period.isTrial !== null) ? period.isTrial : false;
// 生成状态badge
let statusBadge = '';
if (!isTrial) {
statusBadge = '<span class="status-badge status-inactive"><i class="fas fa-pause-circle"></i> 非试用</span>';
} else if (daysUntilExpiry < 0) {
statusBadge = '<span class="status-badge status-expired"><i class="fas fa-times-circle"></i> 已过期</span>';
} else if (daysUntilExpiry === 0) {
statusBadge = '<span class="status-badge status-urgent"><i class="fas fa-exclamation-circle"></i> 今日到期</span>';
} else if (daysUntilExpiry <= 3) {
statusBadge = `<span class="status-badge status-warning"><i class="fas fa-clock"></i> ${daysUntilExpiry}天后到期</span>`;
} else if (daysUntilExpiry <= 7) {
statusBadge = `<span class="status-badge status-notice"><i class="fas fa-bell"></i> ${daysUntilExpiry}天后到期</span>`;
} else {
statusBadge = '<span class="status-badge status-active"><i class="fas fa-check-circle"></i> 试用中</span>';
}
const startTime = formatDateTime(period.startTime);
const endTime = formatDateTime(period.endTime);
const createdAt = formatDateTime(period.createdAt);
row.innerHTML = `
<td><strong>${customerName}</strong></td>
<td>${statusBadge}</td>
<td>${startTime}</td>
<td>${endTime}</td>
<td>${createdAt}</td>
<td class="action-cell">
<button class="action-btn edit-btn" data-id="${period.id}" title="编辑">
<i class="fas fa-edit"></i>
</button>
<button class="action-btn delete-btn" data-id="${period.id}" title="删除">
<i class="fas fa-trash"></i>
</button>
</td>
`;
tbody.appendChild(row);
});
// Add event listeners
tbody.querySelectorAll('.edit-btn').forEach(btn => {
btn.addEventListener('click', function () {
const periodId = this.getAttribute('data-id');
openEditTrialModal(periodId);
});
});
tbody.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', function () {
const periodId = this.getAttribute('data-id');
deleteTrialPeriodFromPage(periodId);
});
});
}
// Update trial pagination
function updateTrialPagination() {
const startItem = trialTotalItems === 0 ? 0 : (trialCurrentPage - 1) * trialPageSize + 1;
const endItem = Math.min(trialCurrentPage * trialPageSize, trialTotalItems);
document.getElementById('trialPaginationInfo').textContent =
`显示 ${startItem}-${endItem}${trialTotalItems}`;
document.getElementById('trialFirstPage').disabled = trialCurrentPage === 1;
document.getElementById('trialPrevPage').disabled = trialCurrentPage === 1;
document.getElementById('trialNextPage').disabled = trialCurrentPage === trialTotalPages;
document.getElementById('trialLastPage').disabled = trialCurrentPage === trialTotalPages;
// Update page numbers
const pageNumbers = document.getElementById('trialPageNumbers');
pageNumbers.innerHTML = '';
const maxVisiblePages = 5;
let startPage = Math.max(1, trialCurrentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(trialTotalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage < maxVisiblePages - 1) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
for (let i = startPage; i <= endPage; i++) {
const pageBtn = document.createElement('button');
pageBtn.className = 'page-number';
if (i === trialCurrentPage) {
pageBtn.classList.add('active');
}
pageBtn.textContent = i;
pageBtn.addEventListener('click', () => {
trialCurrentPage = i;
applyTrialFiltersAndSort();
});
pageNumbers.appendChild(pageBtn);
}
}
// Open add trial modal
function openAddTrialModal() {
console.log('Opening add trial modal');
document.getElementById('trialCustomerInput').value = '';
document.querySelector('input[name="isTrial"][value="true"]').checked = true;
document.getElementById('trialStartTime').value = '';
document.getElementById('trialEndTime').value = '';
document.getElementById('addTrialPeriodModal').style.display = 'block';
}
// Open edit trial modal
function openEditTrialModal(periodId) {
const period = filteredTrialPeriodsData.find(p => p.id === periodId);
if (!period) return;
document.getElementById('editTrialPeriodId').value = period.id;
const startDate = new Date(period.startTime);
const endDate = new Date(period.endTime);
document.getElementById('editTrialStartTime').value = formatDateTimeLocal(startDate);
document.getElementById('editTrialEndTime').value = formatDateTimeLocal(endDate);
document.getElementById('editTrialPeriodModal').style.display = 'block';
}
// Delete trial period from page
async function deleteTrialPeriodFromPage(periodId) {
if (!confirm('确定要删除这个试用时间记录吗?')) {
return;
}
try {
const response = await authenticatedFetch(`/api/trial-periods/${periodId}`, {
method: 'DELETE'
});
if (response.ok) {
await loadAllTrialPeriods();
alert('试用时间删除成功!');
} else {
alert('删除试用时间时出错');
}
} catch (error) {
console.error('Error deleting trial period:', error);
alert('删除试用时间时出错');
}
}
// Create trial period from page
async function createTrialPeriodFromPage() {
const customerName = document.getElementById('trialCustomerInput').value.trim();
const isTrialValue = document.querySelector('input[name="isTrial"]:checked').value;
const isTrial = isTrialValue === 'true';
const startTime = document.getElementById('trialStartTime').value;
const endTime = document.getElementById('trialEndTime').value;
if (!customerName) {
alert('请输入客户名称');
return;
}
if (!startTime || !endTime) {
alert('请填写开始时间和结束时间');
return;
}
// Find or create customer ID from customer name
let customerId = null;
for (const [id, name] of Object.entries(customersMap)) {
if (name === customerName) {
customerId = id;
break;
}
}
// If customer doesn't exist in map, generate a new ID
if (!customerId) {
customerId = customerName; // Use customer name as ID for now
}
const formData = {
customerId: customerId,
startTime: new Date(startTime).toISOString(),
endTime: new Date(endTime).toISOString(),
isTrial: isTrial
};
try {
const response = await authenticatedFetch('/api/trial-periods', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (response.ok) {
document.getElementById('addTrialPeriodModal').style.display = 'none';
document.getElementById('addTrialPeriodForm').reset();
await loadAllTrialPeriods();
alert('试用时间添加成功!');
} else {
alert('添加试用时间时出错');
}
} catch (error) {
console.error('Error creating trial period:', error);
alert('添加试用时间时出错');
}
}
// Initialize when switching to trial periods section
document.addEventListener('DOMContentLoaded', function () {
initTrialPeriodsPage();
// Override the form submit for add trial period
const addForm = document.getElementById('addTrialPeriodForm');
if (addForm) {
addForm.addEventListener('submit', async function (e) {
e.preventDefault();
await createTrialPeriodFromPage();
});
}
});