fix_index

This commit is contained in:
hangyu.tao 2026-01-17 08:22:45 +08:00
parent c8f6341bc9
commit bc0a84511c
4 changed files with 362 additions and 53 deletions

View File

@ -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

View File

@ -358,7 +358,7 @@
</div>
<div class="stat-info">
<h3 id="completedTasks">0</h3>
<p></p>
<p>已成</p>
</div>
</div>
<div class="stat-card stat-card-highlight">
@ -873,6 +873,126 @@
</div>
</div>
<!-- Modal for adding follow-up -->
<div id="addFollowupModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-plus-circle"></i> 添加跟进记录</h3>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<form id="addFollowupModalForm">
<div class="form-row">
<div class="form-group">
<label for="addFollowupCustomerName">客户名称 <span style="color: red;">*</span></label>
<select id="addFollowupCustomerName" name="customerName" required>
<option value="">请选择客户</option>
</select>
</div>
<div class="form-group">
<label for="addFollowupDealStatus">成交状态 <span style="color: red;">*</span></label>
<select id="addFollowupDealStatus" name="dealStatus" required>
<option value="">请选择</option>
<option value="未成交">未成交</option>
<option value="已成交">已成交</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="addFollowupCustomerLevel">客户级别 <span style="color: red;">*</span></label>
<select id="addFollowupCustomerLevel" name="customerLevel" required>
<option value="">请选择</option>
<option value="A">A级 (重点客户)</option>
<option value="B">B级 (潜在客户)</option>
<option value="C">C级 (一般客户)</option>
</select>
</div>
<div class="form-group">
<label for="addFollowupIndustry">客户行业 <span style="color: red;">*</span></label>
<input type="text" id="addFollowupIndustry" name="industry" required>
</div>
</div>
<div class="form-group">
<label for="addFollowupTime">跟进时间 <span style="color: red;">*</span></label>
<input type="datetime-local" id="addFollowupTime" name="followUpTime" required>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">
<i class="fas fa-save"></i>
创建
</button>
<button type="button" class="btn-secondary cancel-add-followup">
<i class="fas fa-times"></i>
取消
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modal for editing follow-up -->
<div id="editFollowupModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-edit"></i> 编辑跟进记录</h3>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<form id="editFollowupForm">
<input type="hidden" id="editFollowupId">
<div class="form-row">
<div class="form-group">
<label for="editFollowupCustomerName">客户名称 <span style="color: red;">*</span></label>
<select id="editFollowupCustomerName" name="customerName" required>
<option value="">请选择客户</option>
</select>
</div>
<div class="form-group">
<label for="editFollowupDealStatus">成交状态 <span style="color: red;">*</span></label>
<select id="editFollowupDealStatus" name="dealStatus" required>
<option value="">请选择</option>
<option value="未成交">未成交</option>
<option value="已成交">已成交</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="editFollowupCustomerLevel">客户级别 <span style="color: red;">*</span></label>
<select id="editFollowupCustomerLevel" name="customerLevel" required>
<option value="">请选择</option>
<option value="A">A级 (重点客户)</option>
<option value="B">B级 (潜在客户)</option>
<option value="C">C级 (一般客户)</option>
</select>
</div>
<div class="form-group">
<label for="editFollowupIndustry">客户行业 <span style="color: red;">*</span></label>
<input type="text" id="editFollowupIndustry" name="industry" required>
</div>
</div>
<div class="form-group">
<label for="editFollowupTime">跟进时间 <span style="color: red;">*</span></label>
<input type="datetime-local" id="editFollowupTime" name="followUpTime" required>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">
<i class="fas fa-save"></i>
更新
</button>
<button type="button" class="btn-secondary cancel-edit-followup">
<i class="fas fa-times"></i>
取消
</button>
</div>
</form>
</div>
</div>
</div>
<script src="/static/js/trial-periods.js"></script>
<script src="/static/js/trial-periods-page.js"></script>
<script src="/static/js/main.js"></script>

View File

@ -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 = '<option value="">请选择客户</option>';
const selectElement = document.getElementById(selectId);
if (!selectElement) return;
selectElement.innerHTML = '<option value="">请选择客户</option>';
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 () {
<td>${formattedTime}</td>
<td>${notificationStatus}</td>
<td>
<button class="action-btn edit-followup-btn" data-id="${followUp.id}">
<i class="fas fa-edit"></i>
</button>
<button class="action-btn delete-followup-btn" data-id="${followUp.id}">
<i class="fas fa-trash"></i>
</button>
@ -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;

View File

@ -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 = '<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>';
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) {