From bc0a84511c4d6bf0807c8e4ef284e06c587920f9 Mon Sep 17 00:00:00 2001 From: "hangyu.tao" Date: Sat, 17 Jan 2026 08:22:45 +0800 Subject: [PATCH] fix_index --- cmd/server/main.go | 85 +++++++------ frontend/index.html | 122 +++++++++++++++++- frontend/js/main.js | 200 +++++++++++++++++++++++++++--- frontend/js/trial-periods-page.js | 8 +- 4 files changed, 362 insertions(+), 53 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 4602312..ef42e5c 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -40,51 +40,21 @@ func main() { }() // Start trial expiry checker in background + // Workday schedule: Monday-Friday, 11:00 AM and 5:00 PM trialChecker := services.NewTrialExpiryChecker(feishuWebhook) go func() { - // Check immediately on startup - trialPeriods, err := trialPeriodStorage.GetAllTrialPeriods() - if err == nil { - customers, err := customerStorage.GetAllCustomers() - if err == nil { - // Create customer name map - customersMap := make(map[string]string) - for _, c := range customers { - customersMap[c.ID] = c.CustomerName - } - - // Convert to services.TrialPeriod type - serviceTrialPeriods := make([]services.TrialPeriod, len(trialPeriods)) - for i, tp := range trialPeriods { - serviceTrialPeriods[i] = services.TrialPeriod{ - ID: tp.ID, - CustomerName: tp.CustomerName, - StartTime: tp.StartTime, - EndTime: tp.EndTime, - CreatedAt: tp.CreatedAt, - } - } - - if err := trialChecker.CheckTrialPeriodsAndNotify(serviceTrialPeriods, customersMap); err != nil { - log.Printf("Error checking trial expiry: %v", err) - } - } - } - - // Then check once per day at 10:00 AM - ticker := time.NewTicker(24 * time.Hour) - defer ticker.Stop() - for range ticker.C { + // Helper function to check and send trial expiry notifications + checkTrialExpiry := func() { trialPeriods, err := trialPeriodStorage.GetAllTrialPeriods() if err != nil { log.Printf("Error loading trial periods for expiry check: %v", err) - continue + return } customers, err := customerStorage.GetAllCustomers() if err != nil { log.Printf("Error loading customers for trial check: %v", err) - continue + return } // Create customer name map @@ -109,6 +79,51 @@ func main() { log.Printf("Error checking trial expiry: %v", err) } } + + // Helper function to check if it's a workday (Monday-Friday) + isWorkday := func(t time.Time) bool { + weekday := t.Weekday() + return weekday >= time.Monday && weekday <= time.Friday + } + + // Helper function to check if current time matches notification time (11:00 or 17:00) + isNotificationTime := func(t time.Time) bool { + hour := t.Hour() + minute := t.Minute() + // Check for 11:00 AM or 5:00 PM (17:00) + return (hour == 11 || hour == 17) && minute == 0 + } + + // Track last notification time to prevent duplicate sends + var lastNotificationTime time.Time + + // Check immediately on startup (only on workdays) + now := time.Now() + if isWorkday(now) { + log.Println("Trial expiry checker: Running initial check on startup...") + checkTrialExpiry() + lastNotificationTime = now + } + + // Check every minute for scheduled notification times + // Workdays: Monday-Friday, 11:00 AM and 5:00 PM + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + for range ticker.C { + now := time.Now() + + // Only send on workdays at 11:00 AM or 5:00 PM + if isWorkday(now) && isNotificationTime(now) { + // Ensure we don't send duplicate notifications for the same time slot + if lastNotificationTime.Hour() != now.Hour() || + lastNotificationTime.Day() != now.Day() || + lastNotificationTime.Month() != now.Month() { + log.Printf("Trial expiry checker: Sending scheduled notification at %s", now.Format("2006-01-02 15:04")) + checkTrialExpiry() + lastNotificationTime = now + } + } + } }() // Enable CORS manually diff --git a/frontend/index.html b/frontend/index.html index 456a4a9..bf062ce 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -358,7 +358,7 @@

0

-

已完成

+

已成交

@@ -873,6 +873,126 @@
+ + + + + + + diff --git a/frontend/js/main.js b/frontend/js/main.js index b59930f..c69160c 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -1140,15 +1140,9 @@ document.addEventListener('DOMContentLoaded', function () { }).map(c => c.customerName).filter(c => c) ).size; - const completed = new Set( - customers.filter(c => - c.statusProgress && (c.statusProgress.includes('已修复') || c.statusProgress.includes('完成') || c.statusProgress.toLowerCase().includes('complete')) - ).map(c => c.customerName).filter(c => c) - ).size; - document.getElementById('totalCustomers').textContent = totalCustomers; document.getElementById('newCustomers').textContent = newCustomers; - document.getElementById('completedTasks').textContent = completed; + // 已成交数量将在 loadFollowUpCount 中更新 } // Load follow-up count for dashboard @@ -1160,12 +1154,23 @@ document.addEventListener('DOMContentLoaded', function () { if (data.followUps) { const followUpCount = data.followUps.length; document.getElementById('followUpCount').textContent = followUpCount; + + // 计算已成交数量:统计 dealStatus 为"已成交"的唯一客户数 + const completedCustomers = new Set( + data.followUps + .filter(f => f.dealStatus === '已成交') + .map(f => f.customerName) + .filter(name => name) + ).size; + document.getElementById('completedTasks').textContent = completedCustomers; } else { document.getElementById('followUpCount').textContent = '0'; + document.getElementById('completedTasks').textContent = '0'; } } catch (error) { console.error('Error loading follow-up count:', error); document.getElementById('followUpCount').textContent = '0'; + document.getElementById('completedTasks').textContent = '0'; } } @@ -1665,14 +1670,45 @@ document.addEventListener('DOMContentLoaded', function () { let followupTotalPages = 1; let followupTotalItems = 0; - // Show/hide follow-up form + // 弹窗相关元素 + const addFollowupModal = document.getElementById('addFollowupModal'); + const addFollowupModalForm = document.getElementById('addFollowupModalForm'); + const editFollowupModal = document.getElementById('editFollowupModal'); + const editFollowupForm = document.getElementById('editFollowupForm'); + + // 点击添加跟进按钮 - 弹出弹窗 if (addFollowUpBtn) { addFollowUpBtn.addEventListener('click', async function () { - followupFormCard.style.display = 'block'; - await loadCustomerListForFollowup(); + await loadCustomerListForFollowupModal(); + addFollowupModal.style.display = 'block'; }); } + // 取消添加跟进弹窗 + if (addFollowupModal) { + addFollowupModal.querySelector('.close').addEventListener('click', () => { + addFollowupModal.style.display = 'none'; + addFollowupModalForm.reset(); + }); + addFollowupModal.querySelector('.cancel-add-followup').addEventListener('click', () => { + addFollowupModal.style.display = 'none'; + addFollowupModalForm.reset(); + }); + } + + // 取消编辑跟进弹窗 + if (editFollowupModal) { + editFollowupModal.querySelector('.close').addEventListener('click', () => { + editFollowupModal.style.display = 'none'; + editFollowupForm.reset(); + }); + editFollowupModal.querySelector('.cancel-edit-followup').addEventListener('click', () => { + editFollowupModal.style.display = 'none'; + editFollowupForm.reset(); + }); + } + + // 旧表单相关逻辑保留兼容 if (cancelFollowupBtn) { cancelFollowupBtn.addEventListener('click', function () { followupFormCard.style.display = 'none'; @@ -1680,19 +1716,22 @@ document.addEventListener('DOMContentLoaded', function () { }); } - // Load customer list for follow-up dropdown from trial periods - async function loadCustomerListForFollowup() { + // Load customer list for follow-up dropdown from trial periods (for modal) + async function loadCustomerListForFollowupModal(selectId = 'addFollowupCustomerName') { try { const response = await authenticatedFetch('/api/trial-customers/list'); const data = await response.json(); - followupCustomerNameSelect.innerHTML = ''; + const selectElement = document.getElementById(selectId); + if (!selectElement) return; + + selectElement.innerHTML = ''; if (data.customerNames && data.customerNames.length > 0) { data.customerNames.forEach(customerName => { const option = document.createElement('option'); option.value = customerName; option.textContent = customerName; - followupCustomerNameSelect.appendChild(option); + selectElement.appendChild(option); }); } } catch (error) { @@ -1803,6 +1842,9 @@ document.addEventListener('DOMContentLoaded', function () { ${formattedTime} ${notificationStatus} + @@ -1812,6 +1854,14 @@ document.addEventListener('DOMContentLoaded', function () { followupTableBody.appendChild(row); }); + // Add edit event listeners + document.querySelectorAll('.edit-followup-btn').forEach(btn => { + btn.addEventListener('click', async function () { + const followUpId = this.getAttribute('data-id'); + await openEditFollowupModal(followUpId); + }); + }); + // Add delete event listeners document.querySelectorAll('.delete-followup-btn').forEach(btn => { btn.addEventListener('click', function () { @@ -1844,6 +1894,128 @@ document.addEventListener('DOMContentLoaded', function () { } } + // 添加跟进弹窗表单提交 + if (addFollowupModalForm) { + addFollowupModalForm.addEventListener('submit', async function (e) { + e.preventDefault(); + + const followUpTimeValue = document.getElementById('addFollowupTime').value; + const followUpTimeISO = new Date(followUpTimeValue).toISOString(); + + const customerSelect = document.getElementById('addFollowupCustomerName'); + const selectedOption = customerSelect.options[customerSelect.selectedIndex]; + const customerName = selectedOption ? selectedOption.textContent : ''; + + const formData = { + customerName: customerName, + dealStatus: document.getElementById('addFollowupDealStatus').value, + customerLevel: document.getElementById('addFollowupCustomerLevel').value, + industry: document.getElementById('addFollowupIndustry').value, + followUpTime: followUpTimeISO + }; + + try { + const response = await authenticatedFetch('/api/followups', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + if (response.ok) { + addFollowupModalForm.reset(); + addFollowupModal.style.display = 'none'; + loadFollowUps(); + alert('跟进记录创建成功!'); + } else { + alert('创建跟进记录时出错'); + } + } catch (error) { + console.error('Error creating follow-up:', error); + alert('创建跟进记录时出错'); + } + }); + } + + // 打开编辑跟进弹窗 + async function openEditFollowupModal(followUpId) { + // 从缓存中查找跟进记录 + const followUp = allFollowUps.find(f => f.id === followUpId); + if (!followUp) { + alert('未找到跟进记录'); + return; + } + + // 加载客户列表到编辑弹窗 + await loadCustomerListForFollowupModal('editFollowupCustomerName'); + + // 填充表单 + document.getElementById('editFollowupId').value = followUp.id; + document.getElementById('editFollowupCustomerName').value = followUp.customerName || ''; + document.getElementById('editFollowupDealStatus').value = followUp.dealStatus || ''; + document.getElementById('editFollowupCustomerLevel').value = followUp.customerLevel || ''; + document.getElementById('editFollowupIndustry').value = followUp.industry || ''; + + // 格式化时间为 datetime-local 格式 + if (followUp.followUpTime) { + const date = new Date(followUp.followUpTime); + const localDateTime = date.getFullYear() + '-' + + String(date.getMonth() + 1).padStart(2, '0') + '-' + + String(date.getDate()).padStart(2, '0') + 'T' + + String(date.getHours()).padStart(2, '0') + ':' + + String(date.getMinutes()).padStart(2, '0'); + document.getElementById('editFollowupTime').value = localDateTime; + } + + editFollowupModal.style.display = 'block'; + } + + // 编辑跟进表单提交 + if (editFollowupForm) { + editFollowupForm.addEventListener('submit', async function (e) { + e.preventDefault(); + + const followUpId = document.getElementById('editFollowupId').value; + const followUpTimeValue = document.getElementById('editFollowupTime').value; + const followUpTimeISO = new Date(followUpTimeValue).toISOString(); + + const customerSelect = document.getElementById('editFollowupCustomerName'); + const selectedOption = customerSelect.options[customerSelect.selectedIndex]; + const customerName = selectedOption ? selectedOption.textContent : ''; + + const formData = { + customerName: customerName, + dealStatus: document.getElementById('editFollowupDealStatus').value, + customerLevel: document.getElementById('editFollowupCustomerLevel').value, + industry: document.getElementById('editFollowupIndustry').value, + followUpTime: followUpTimeISO + }; + + try { + const response = await authenticatedFetch(`/api/followups/${followUpId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + if (response.ok) { + editFollowupForm.reset(); + editFollowupModal.style.display = 'none'; + loadFollowUps(); + alert('跟进记录更新成功!'); + } else { + alert('更新跟进记录时出错'); + } + } catch (error) { + console.error('Error updating follow-up:', error); + alert('更新跟进记录时出错'); + } + }); + } + // Update follow-up pagination controls function updateFollowupPaginationControls() { const startItem = followupTotalItems === 0 ? 0 : (followupCurrentPage - 1) * followupPageSize + 1; diff --git a/frontend/js/trial-periods-page.js b/frontend/js/trial-periods-page.js index 21c8873..7dc6eb9 100644 --- a/frontend/js/trial-periods-page.js +++ b/frontend/js/trial-periods-page.js @@ -296,11 +296,13 @@ function renderTrialPeriodsTable() { // 直接使用 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.ceil((endDate - now) / (1000 * 60 * 60 * 24)); + const daysUntilExpiry = Math.round((endDateOnly - today) / (1000 * 60 * 60 * 24)); const isTrial = (period.isTrial !== undefined && period.isTrial !== null) ? period.isTrial : false; // 生成状态badge @@ -308,7 +310,7 @@ function renderTrialPeriodsTable() { if (!isTrial) { statusBadge = ' 非试用'; } else if (daysUntilExpiry < 0) { - statusBadge = ' 已过期'; + statusBadge = ' 已到期'; } else if (daysUntilExpiry === 0) { statusBadge = ' 今日到期'; } else if (daysUntilExpiry <= 3) {