crm/frontend/js/trial-periods-page.js
2026-01-16 18:59:19 +08:00

524 lines
18 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 {
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();
});
}
});