524 lines
18 KiB
JavaScript
524 lines
18 KiB
JavaScript
// 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 {
|
||
const response = await authenticatedFetch('/api/customers/list');
|
||
|
||
if (!response) {
|
||
console.error('No response from API');
|
||
return;
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
// Use the complete customer map for ID->Name lookups
|
||
customersMap = data.customerMap || {};
|
||
|
||
// Also populate the customer select dropdown
|
||
populateCustomerSelect();
|
||
} catch (error) {
|
||
console.error('Error loading customers map:', error);
|
||
}
|
||
}
|
||
|
||
// Populate the customer select dropdown
|
||
function populateCustomerSelect() {
|
||
const select = document.getElementById('trialCustomerSelect');
|
||
if (!select) return;
|
||
|
||
select.innerHTML = '<option value="">-- 选择已有客户或输入新客户 --</option>';
|
||
|
||
// Get unique names from customersMap
|
||
const names = [...new Set(Object.values(customersMap))].sort();
|
||
names.forEach(name => {
|
||
const option = document.createElement('option');
|
||
option.value = name;
|
||
option.textContent = name;
|
||
select.appendChild(option);
|
||
});
|
||
}
|
||
|
||
// Helper function to get customer name - returns friendly placeholder if not found
|
||
function getCustomerName(customerId) {
|
||
if (!customerId) return '未知客户';
|
||
const name = customersMap[customerId];
|
||
if (name) return name;
|
||
// If ID not found in map, return shortened ID with indicator
|
||
return '(已删除客户)';
|
||
}
|
||
|
||
// 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));
|
||
// 直接使用 customerName 字段
|
||
const customerName = period.customerName || '未知客户';
|
||
|
||
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');
|
||
|
||
// 直接使用 customerName 字段
|
||
const customerName = period.customerName || '未知客户';
|
||
|
||
// 计算状态和到期天数
|
||
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() {
|
||
const inputEl = document.getElementById('trialCustomerInput');
|
||
if (inputEl) inputEl.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);
|
||
|
||
// 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';
|
||
}
|
||
|
||
// 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 inputEl = document.getElementById('trialCustomerInput');
|
||
|
||
// Get customer name from input
|
||
const customerName = inputEl ? inputEl.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;
|
||
}
|
||
|
||
// 直接使用 customerName,不再需要查找或创建 customerId
|
||
const formData = {
|
||
customerName: customerName,
|
||
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();
|
||
});
|
||
}
|
||
});
|