This commit is contained in:
hangyu.tao 2026-01-16 18:59:19 +08:00
parent af769258d2
commit 8cbad66b32
8 changed files with 98 additions and 105 deletions

View File

@ -58,7 +58,7 @@ func main() {
for i, tp := range trialPeriods { for i, tp := range trialPeriods {
serviceTrialPeriods[i] = services.TrialPeriod{ serviceTrialPeriods[i] = services.TrialPeriod{
ID: tp.ID, ID: tp.ID,
CustomerID: tp.CustomerID, CustomerName: tp.CustomerName,
StartTime: tp.StartTime, StartTime: tp.StartTime,
EndTime: tp.EndTime, EndTime: tp.EndTime,
CreatedAt: tp.CreatedAt, CreatedAt: tp.CreatedAt,
@ -98,7 +98,7 @@ func main() {
for i, tp := range trialPeriods { for i, tp := range trialPeriods {
serviceTrialPeriods[i] = services.TrialPeriod{ serviceTrialPeriods[i] = services.TrialPeriod{
ID: tp.ID, ID: tp.ID,
CustomerID: tp.CustomerID, CustomerName: tp.CustomerName,
StartTime: tp.StartTime, StartTime: tp.StartTime,
EndTime: tp.EndTime, EndTime: tp.EndTime,
CreatedAt: tp.CreatedAt, CreatedAt: tp.CreatedAt,

View File

@ -253,8 +253,7 @@
<!-- Trial Periods Section --> <!-- Trial Periods Section -->
<section id="trialPeriodsSection" class="content-section"> <section id="trialPeriodsSection" class="content-section">
<div class="section-title"> <div class="section-title" style="justify-content: flex-end;">
<h2><i class="fas fa-clock"></i> 客户信息</h2>
<button id="addTrialBtn" class="btn-primary"> <button id="addTrialBtn" class="btn-primary">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
添加试用时间 添加试用时间

View File

@ -1096,34 +1096,30 @@ document.addEventListener('DOMContentLoaded', function () {
const customersData = await customersResponse.json(); const customersData = await customersResponse.json();
const customersMap = customersData.customerMap || {}; const customersMap = customersData.customerMap || {};
// Collect unique trial customer names - only include valid customer names // Collect unique trial customer names - directly use customerName field
const trialCustomerNames = new Set(); const trialCustomerNames = new Set();
trialPeriods.forEach(period => { trialPeriods.forEach(period => {
// Only add if customerId exists in map (valid customer) // 直接使用 customerName 字段
const customerName = customersMap[period.customerId]; if (period.customerName) {
if (customerName) { trialCustomerNames.add(period.customerName);
trialCustomerNames.add(customerName);
} }
// If customerId not in map, skip it (deleted customer) - don't add UUID as name
}); });
// Update total customers to include trial customers // Update total customers display (数据仪表盘的总客户数从每周进度获取)
// This function will be called after loading dashboard customers
updateTotalCustomersWithTrialData(trialCustomerNames); updateTotalCustomersWithTrialData(trialCustomerNames);
} catch (error) { } catch (error) {
console.error('Error loading trial customers for dashboard:', error); console.error('Error loading trial customers for dashboard:', error);
} }
} }
// Update total customers count including trial customers // Update total customers count - 数据仪表盘的总客户数取的是每周进度表里的客户数量
function updateTotalCustomersWithTrialData(trialCustomerNames) { function updateTotalCustomersWithTrialData(trialCustomerNames) {
// Get current dashboard customers // Get current dashboard customers (from customers.json / 每周进度)
const progressCustomerNames = new Set(dashboardCustomers.map(c => c.customerName).filter(c => c)); const progressCustomerNames = new Set(dashboardCustomers.map(c => c.customerName).filter(c => c));
// Merge both sets // 数据仪表盘的总客户数取的是每周进度表里的客户数量,支持去重
const allCustomerNames = new Set([...progressCustomerNames, ...trialCustomerNames]); document.getElementById('totalCustomers').textContent = progressCustomerNames.size;
// Update the total customers display
document.getElementById('totalCustomers').textContent = allCustomerNames.size;
} }
// Update dashboard statistics // Update dashboard statistics

View File

@ -94,11 +94,31 @@ async function loadCustomersMap() {
// Use the complete customer map for ID->Name lookups // Use the complete customer map for ID->Name lookups
customersMap = data.customerMap || {}; customersMap = data.customerMap || {};
// Also populate the customer select dropdown
populateCustomerSelect();
} catch (error) { } catch (error) {
console.error('Error loading customers map:', 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 // Helper function to get customer name - returns friendly placeholder if not found
function getCustomerName(customerId) { function getCustomerName(customerId) {
if (!customerId) return '未知客户'; if (!customerId) return '未知客户';
@ -182,7 +202,8 @@ function renderExpiryWarnings() {
endTime.setHours(0, 0, 0, 0); // Set to start of day endTime.setHours(0, 0, 0, 0); // Set to start of day
const daysUntilExpiry = Math.ceil((endTime - now) / (1000 * 60 * 60 * 24)); const daysUntilExpiry = Math.ceil((endTime - now) / (1000 * 60 * 60 * 24));
const customerName = getCustomerName(period.customerId); // 直接使用 customerName 字段
const customerName = period.customerName || '未知客户';
if (daysUntilExpiry >= 0 && daysUntilExpiry <= 3) { if (daysUntilExpiry >= 0 && daysUntilExpiry <= 3) {
warnings.push({ warnings.push({
@ -272,8 +293,8 @@ function renderTrialPeriodsTable() {
const row = document.createElement('tr'); const row = document.createElement('tr');
row.classList.add('trial-row'); row.classList.add('trial-row');
// 显示客户名称如果找不到则显示ID // 直接使用 customerName 字段
const customerName = getCustomerName(period.customerId); const customerName = period.customerName || '未知客户';
// 计算状态和到期天数 // 计算状态和到期天数
const now = new Date(); const now = new Date();
@ -379,7 +400,9 @@ function updateTrialPagination() {
// Open add trial modal // Open add trial modal
function openAddTrialModal() { function openAddTrialModal() {
document.getElementById('trialCustomerInput').value = ''; const inputEl = document.getElementById('trialCustomerInput');
if (inputEl) inputEl.value = '';
document.querySelector('input[name="isTrial"][value="true"]').checked = true; document.querySelector('input[name="isTrial"][value="true"]').checked = true;
document.getElementById('trialStartTime').value = ''; document.getElementById('trialStartTime').value = '';
document.getElementById('trialEndTime').value = ''; document.getElementById('trialEndTime').value = '';
@ -434,14 +457,18 @@ async function deleteTrialPeriodFromPage(periodId) {
// Create trial period from page // Create trial period from page
async function createTrialPeriodFromPage() { async function createTrialPeriodFromPage() {
const customerName = document.getElementById('trialCustomerInput').value.trim(); 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 isTrialValue = document.querySelector('input[name="isTrial"]:checked').value;
const isTrial = isTrialValue === 'true'; const isTrial = isTrialValue === 'true';
const startTime = document.getElementById('trialStartTime').value; const startTime = document.getElementById('trialStartTime').value;
const endTime = document.getElementById('trialEndTime').value; const endTime = document.getElementById('trialEndTime').value;
if (!customerName) { if (!customerName) {
alert('请输入客户名称'); alert('请选择或输入客户名称');
return; return;
} }
@ -450,22 +477,9 @@ async function createTrialPeriodFromPage() {
return; return;
} }
// Find or create customer ID from customer name // 直接使用 customerName不再需要查找或创建 customerId
let customerId = null;
for (const [id, name] of Object.entries(customersMap)) {
if (name === customerName) {
customerId = id;
break;
}
}
// If customer doesn't exist in map, generate a new ID
if (!customerId) {
customerId = customerName; // Use customer name as ID for now
}
const formData = { const formData = {
customerId: customerId, customerName: customerName,
startTime: new Date(startTime).toISOString(), startTime: new Date(startTime).toISOString(),
endTime: new Date(endTime).toISOString(), endTime: new Date(endTime).toISOString(),
isTrial: isTrial isTrial: isTrial

View File

@ -83,7 +83,7 @@ func (h *TrialPeriodHandler) CreateTrialPeriod(w http.ResponseWriter, r *http.Re
} }
trialPeriod := models.TrialPeriod{ trialPeriod := models.TrialPeriod{
CustomerID: req.CustomerID, CustomerName: req.CustomerName,
StartTime: startTime, StartTime: startTime,
EndTime: endTime, EndTime: endTime,
IsTrial: req.IsTrial, IsTrial: req.IsTrial,
@ -98,7 +98,7 @@ func (h *TrialPeriodHandler) CreateTrialPeriod(w http.ResponseWriter, r *http.Re
// Send Feishu notification // Send Feishu notification
if h.feishuWebhook != "" { if h.feishuWebhook != "" {
go h.sendTrialNotification(req.CustomerID, startTime, endTime) go h.sendTrialNotification(req.CustomerName, startTime, endTime)
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -152,7 +152,7 @@ func (h *TrialPeriodHandler) UpdateTrialPeriod(w http.ResponseWriter, r *http.Re
} else { } else {
endTime = existingPeriod.EndTime endTime = existingPeriod.EndTime
} }
go h.sendTrialNotification(existingPeriod.CustomerID, startTime, endTime) go h.sendTrialNotification(existingPeriod.CustomerName, startTime, endTime)
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@ -182,7 +182,7 @@ func (h *TrialPeriodHandler) DeleteTrialPeriod(w http.ResponseWriter, r *http.Re
} }
// sendTrialNotification sends a rich Feishu card notification // sendTrialNotification sends a rich Feishu card notification
func (h *TrialPeriodHandler) sendTrialNotification(customerID string, startTime, endTime time.Time) { func (h *TrialPeriodHandler) sendTrialNotification(customerName string, startTime, endTime time.Time) {
if h.feishuWebhook == "" { if h.feishuWebhook == "" {
return return
} }
@ -198,12 +198,8 @@ func (h *TrialPeriodHandler) sendTrialNotification(customerID string, startTime,
return return
} }
// Get customer name // customerName is already passed as parameter, no need to lookup
customerName := customerID // Just use it directly for notification
customer, err := h.customerStorage.GetCustomerByID(customerID)
if err == nil && customer != nil {
customerName = customer.CustomerName
}
// Determine urgency level and message based on days until expiry // Determine urgency level and message based on days until expiry
var title, urgencyLevel, urgencyColor, expiryText string var title, urgencyLevel, urgencyColor, expiryText string
@ -324,42 +320,29 @@ func (h *TrialPeriodHandler) sendTrialNotification(customerID string, startTime,
return return
} }
log.Printf("Sent trial notification for customer: %s (expires in %d days)", customerID, daysUntilExpiry) log.Printf("Sent trial notification for customer: %s (expires in %d days)", customerName, daysUntilExpiry)
} }
// GetTrialCustomerList returns a list of unique customer names from trial periods // GetTrialCustomerList returns a list of unique customer names from trial_periods
// This is the data source for dropdown lists in 每周进度 and 客户跟进
func (h *TrialPeriodHandler) GetTrialCustomerList(w http.ResponseWriter, r *http.Request) { func (h *TrialPeriodHandler) GetTrialCustomerList(w http.ResponseWriter, r *http.Request) {
// Get all trial periods
trialPeriods, err := h.storage.GetAllTrialPeriods() trialPeriods, err := h.storage.GetAllTrialPeriods()
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
// Create a set of unique customer IDs // Get unique customer names from trial periods
customerIDs := make(map[string]bool)
for _, period := range trialPeriods {
if period.CustomerID != "" {
customerIDs[period.CustomerID] = true
}
}
// Get unique customer names (only include customers that exist in storage)
seenNames := make(map[string]bool) seenNames := make(map[string]bool)
var customerNames []string var customerNames []string
for customerID := range customerIDs { for _, period := range trialPeriods {
// Try to get customer name from customer storage if period.CustomerName != "" && !seenNames[period.CustomerName] {
customer, err := h.customerStorage.GetCustomerByID(customerID) customerNames = append(customerNames, period.CustomerName)
if err == nil && customer != nil && customer.CustomerName != "" { seenNames[period.CustomerName] = true
customerName := customer.CustomerName
// Add to list if not seen before
if !seenNames[customerName] {
customerNames = append(customerNames, customerName)
seenNames[customerName] = true
} }
} }
// Skip customers that don't exist in storage (don't show ID as name)
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{

View File

@ -64,7 +64,7 @@ func (ts *trialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string) ([]
var customerPeriods []models.TrialPeriod var customerPeriods []models.TrialPeriod
for _, period := range allPeriods { for _, period := range allPeriods {
if period.CustomerID == customerID { if period.CustomerName == customerID {
customerPeriods = append(customerPeriods, period) customerPeriods = append(customerPeriods, period)
} }
} }

View File

@ -2,10 +2,10 @@ package models
import "time" import "time"
// TrialPeriod represents a trial period for a customer // TrialPeriod represents a trial period for a customer (客户信息)
type TrialPeriod struct { type TrialPeriod struct {
ID string `json:"id"` ID string `json:"id"`
CustomerID string `json:"customerId"` CustomerName string `json:"customerName"` // 直接存储客户名称
StartTime time.Time `json:"startTime"` StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"` EndTime time.Time `json:"endTime"`
IsTrial bool `json:"isTrial"` IsTrial bool `json:"isTrial"`
@ -14,7 +14,7 @@ type TrialPeriod struct {
// CreateTrialPeriodRequest represents the request to create a trial period // CreateTrialPeriodRequest represents the request to create a trial period
type CreateTrialPeriodRequest struct { type CreateTrialPeriodRequest struct {
CustomerID string `json:"customerId"` CustomerName string `json:"customerName"` // 直接使用客户名称
StartTime string `json:"startTime"` StartTime string `json:"startTime"`
EndTime string `json:"endTime"` EndTime string `json:"endTime"`
IsTrial bool `json:"isTrial"` IsTrial bool `json:"isTrial"`
@ -22,6 +22,7 @@ type CreateTrialPeriodRequest struct {
// UpdateTrialPeriodRequest represents the request to update a trial period // UpdateTrialPeriodRequest represents the request to update a trial period
type UpdateTrialPeriodRequest struct { type UpdateTrialPeriodRequest struct {
CustomerName *string `json:"customerName,omitempty"`
StartTime *string `json:"startTime,omitempty"` StartTime *string `json:"startTime,omitempty"`
EndTime *string `json:"endTime,omitempty"` EndTime *string `json:"endTime,omitempty"`
IsTrial *bool `json:"isTrial,omitempty"` IsTrial *bool `json:"isTrial,omitempty"`

View File

@ -41,7 +41,7 @@ type Customer struct {
// TrialPeriod represents a trial period record // TrialPeriod represents a trial period record
type TrialPeriod struct { type TrialPeriod struct {
ID string `json:"id"` ID string `json:"id"`
CustomerID string `json:"customerId"` CustomerName string `json:"customerName"`
StartTime time.Time `json:"startTime"` StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"` EndTime time.Time `json:"endTime"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
@ -151,9 +151,9 @@ func (t *TrialExpiryChecker) CheckTrialPeriodsAndNotify(trialPeriods []TrialPeri
// Send notification for trials expiring today, tomorrow, or within 3 days // Send notification for trials expiring today, tomorrow, or within 3 days
if daysUntilExpiry >= 0 && daysUntilExpiry <= 3 { if daysUntilExpiry >= 0 && daysUntilExpiry <= 3 {
customerName := customersMap[period.CustomerID] customerName := period.CustomerName
if customerName == "" { if customerName == "" {
customerName = period.CustomerID customerName = "未知客户"
} }
trialPeriodStr := fmt.Sprintf("%s%s", trialPeriodStr := fmt.Sprintf("%s%s",