// 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(); }); // Intended product checkbox change handlers setupIntendedProductCheckboxes('trialIntendedProductOtherCheckbox', 'trialIntendedProductOther'); setupIntendedProductCheckboxes('editTrialIntendedProductOtherCheckbox', 'editTrialIntendedProductOther'); // 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 = ''; // 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(); } // Setup intended product checkboxes with "other" option handling function setupIntendedProductCheckboxes(otherCheckboxId, otherInputId) { const otherCheckbox = document.getElementById(otherCheckboxId); const otherInput = document.getElementById(otherInputId); if (!otherCheckbox || !otherInput) return; otherCheckbox.addEventListener('change', function () { if (this.checked) { otherInput.style.display = 'block'; otherInput.focus(); } else { otherInput.style.display = 'none'; otherInput.value = ''; } }); } // Get intended product values from checkboxes (returns comma-separated string) function getIntendedProductValues(checkboxName, otherInputId) { const checkboxes = document.querySelectorAll(`input[name="${checkboxName}"]:checked`); const otherInput = document.getElementById(otherInputId); const values = []; checkboxes.forEach(cb => { if (cb.value === '其他') { // If "other" is checked, use the custom input value if (otherInput && otherInput.value.trim()) { values.push(otherInput.value.trim()); } } else { values.push(cb.value); } }); return values.join(', '); } // Set intended product values in checkboxes (accepts comma-separated string) function setIntendedProductValues(checkboxName, otherInputId, value) { const checkboxes = document.querySelectorAll(`input[name="${checkboxName}"]`); const otherInput = document.getElementById(otherInputId); // Reset all checkboxes checkboxes.forEach(cb => { cb.checked = false; }); if (otherInput) { otherInput.style.display = 'none'; otherInput.value = ''; } if (!value) return; // Parse comma-separated values const selectedValues = value.split(',').map(v => v.trim()).filter(v => v); const predefinedOptions = ['数据闭环', 'robogo']; let hasOther = false; const otherValues = []; selectedValues.forEach(val => { if (predefinedOptions.includes(val)) { // Check the corresponding checkbox checkboxes.forEach(cb => { if (cb.value === val) { cb.checked = true; } }); } else { // Custom value - mark as "other" hasOther = true; otherValues.push(val); } }); // Handle "other" checkbox and input if (hasOther) { checkboxes.forEach(cb => { if (cb.value === '其他') { cb.checked = true; } }); if (otherInput) { otherInput.style.display = 'block'; otherInput.value = otherValues.join(', '); } } } // Legacy compatibility functions (for backward compatibility) function setupIntendedProductDropdown(selectId, otherId) { // No-op for backward compatibility - now using checkbox groups } function getIntendedProductValue(selectId, otherId) { // Map to new checkbox-based function based on the select ID if (selectId === 'trialIntendedProduct') { return getIntendedProductValues('intendedProduct', 'trialIntendedProductOther'); } else if (selectId === 'editTrialIntendedProduct') { return getIntendedProductValues('editIntendedProduct', 'editTrialIntendedProductOther'); } return ''; } function setIntendedProductValue(selectId, otherId, value) { // Map to new checkbox-based function based on the select ID if (selectId === 'trialIntendedProduct') { setIntendedProductValues('intendedProduct', 'trialIntendedProductOther', value); } else if (selectId === 'editTrialIntendedProduct') { setIntendedProductValues('editIntendedProduct', 'editTrialIntendedProductOther', value); } } // 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 = `${warning.customerName} 客户的试用期将于今天到期,请及时跟进!`; } else if (warning.daysUntilExpiry === 1) { warningClass = 'warning-tomorrow'; iconClass = 'fa-exclamation-circle'; title = '试用明日到期'; message = `${warning.customerName} 客户的试用期将于明天到期,请及时跟进!`; } else { warningClass = 'warning-soon'; iconClass = 'fa-info-circle'; title = '试用即将到期'; message = `${warning.customerName} 客户的试用期将于${warning.daysUntilExpiry}天后到期,请及时跟进!`; } const formattedEndTime = formatDateTime(warning.endTime); message += `
试用结束时间:${formattedEndTime}`; card.className = `expiry-warning-card ${warningClass}`; card.innerHTML = `
${title}
${message}
`; 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 = `

👥 还没有客户试用信息

点击上方「添加客户」开始管理客户试用

`; 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 today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); // 今天 00:00:00 const endDate = new Date(period.endTime); const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()); // 结束日期 00:00:00 const startDate = new Date(period.startTime); const daysUntilExpiry = Math.round((endDateOnly - today) / (1000 * 60 * 60 * 24)); const isTrial = (period.isTrial !== undefined && period.isTrial !== null) ? period.isTrial : false; // 生成状态badge let statusBadge = ''; if (!isTrial) { statusBadge = ' 非试用'; } else if (daysUntilExpiry < 0) { statusBadge = ' 已到期'; } else if (daysUntilExpiry === 0) { statusBadge = ' 今日到期'; } else if (daysUntilExpiry <= 3) { statusBadge = ` ${daysUntilExpiry}天后到期`; } else if (daysUntilExpiry <= 7) { statusBadge = ` ${daysUntilExpiry}天后到期`; } else { statusBadge = ' 试用中'; } const startTime = formatDateTime(period.startTime); const endTime = formatDateTime(period.endTime); const createdAt = formatDateTime(period.createdAt); const source = period.source || ''; const intendedProduct = period.intendedProduct || ''; row.innerHTML = ` ${customerName} ${source} ${intendedProduct} ${statusBadge} ${startTime} ${endTime} ${createdAt} `; 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:not(.disabled)').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 = ''; const sourceEl = document.getElementById('trialCustomerSource'); if (sourceEl) sourceEl.value = ''; // Reset intended product setIntendedProductValue('trialIntendedProduct', 'trialIntendedProductOther', ''); 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; // Set customer source const sourceEl = document.getElementById('editTrialCustomerSource'); if (sourceEl) sourceEl.value = period.source || ''; // Set intended product setIntendedProductValue('editTrialIntendedProduct', 'editTrialIntendedProductOther', period.intendedProduct || ''); 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(); } 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'); const sourceEl = document.getElementById('trialCustomerSource'); // Get customer name from input const customerName = inputEl ? inputEl.value.trim() : ''; // Get customer source from input const source = sourceEl ? sourceEl.value.trim() : ''; // Get intended product const intendedProduct = getIntendedProductValue('trialIntendedProduct', 'trialIntendedProductOther'); 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, source: source, intendedProduct: intendedProduct, 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(); } 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(); }); } });