This commit is contained in:
hangyu.tao 2026-01-15 21:14:12 +08:00
parent 569ca8479f
commit 9952f1569e
8 changed files with 1445 additions and 139 deletions

View File

@ -256,6 +256,15 @@ func main() {
}
})
// Get unique customer list from trial periods
http.HandleFunc("/api/trial-customers/list", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
trialPeriodHandler.GetTrialCustomerList(w, r)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
http.HandleFunc("/api/trial-periods/", func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path

View File

@ -120,6 +120,45 @@ body {
overflow-y: auto;
}
/* Navigation Group Styles */
.nav-group {
margin-bottom: 10px;
}
.nav-group-title {
display: flex;
align-items: center;
padding: 12px 20px;
color: rgba(255, 255, 255, 0.5);
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
gap: 10px;
}
.nav-group-title i {
font-size: 0.9rem;
color: var(--primary-orange);
}
.nav-group .nav-item {
padding-left: 35px;
}
.sidebar.collapsed .nav-group-title span {
display: none;
}
.sidebar.collapsed .nav-group-title {
justify-content: center;
padding: 12px;
}
.sidebar.collapsed .nav-group .nav-item {
padding-left: 15px;
}
.nav-item {
display: flex;
align-items: center;
@ -1401,4 +1440,726 @@ td.overflow-cell {
.pagination-info {
text-align: center;
}
}
/* Status Badge Styles */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
white-space: nowrap;
}
.status-badge i {
font-size: 0.75rem;
}
.status-active {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-inactive {
background-color: #e2e3e5;
color: #383d41;
border: 1px solid #d6d8db;
}
.status-expired {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status-urgent {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
animation: pulse 1.5s infinite;
}
.status-warning {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeeba;
}
.status-notice {
background-color: #cce5ff;
color: #004085;
border: 1px solid #b8daff;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
/* Progress Status Badges */
.progress-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 15px;
font-size: 0.75rem;
font-weight: 600;
}
.progress-completed {
background-color: #d4edda;
color: #155724;
}
.progress-in-progress {
background-color: #cce5ff;
color: #004085;
}
.progress-pending {
background-color: #fff3cd;
color: #856404;
}
.progress-rejected {
background-color: #f8d7da;
color: #721c24;
}
.progress-launched {
background-color: #d1ecf1;
color: #0c5460;
}
/* Quick Action Cards for Dashboard */
.quick-actions {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 25px;
}
@media (max-width: 1200px) {
.quick-actions {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.quick-actions {
grid-template-columns: 1fr;
}
}
.quick-action-card {
background: linear-gradient(135deg, var(--white) 0%, #f8f9fa 100%);
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
gap: 15px;
box-shadow: var(--shadow-sm);
cursor: pointer;
transition: all 0.3s ease;
border-left: 4px solid transparent;
}
.quick-action-card:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-md);
}
.quick-action-card.alert {
border-left-color: #dc3545;
}
.quick-action-card.warning {
border-left-color: #ffc107;
}
.quick-action-card.info {
border-left-color: #17a2b8;
}
.quick-action-card.success {
border-left-color: #28a745;
}
.quick-action-icon {
width: 50px;
height: 50px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
}
.quick-action-card.alert .quick-action-icon {
background-color: rgba(220, 53, 69, 0.1);
color: #dc3545;
}
.quick-action-card.warning .quick-action-icon {
background-color: rgba(255, 193, 7, 0.1);
color: #ffc107;
}
.quick-action-card.info .quick-action-icon {
background-color: rgba(23, 162, 184, 0.1);
color: #17a2b8;
}
.quick-action-card.success .quick-action-icon {
background-color: rgba(40, 167, 69, 0.1);
color: #28a745;
}
.quick-action-content h4 {
margin: 0 0 5px 0;
font-size: 0.9rem;
color: var(--dark-gray);
}
.quick-action-content .count {
font-size: 1.8rem;
font-weight: 700;
color: var(--dark-gray);
}
.quick-action-content .label {
font-size: 0.8rem;
color: var(--medium-gray);
}
/* Empty State Styles */
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--medium-gray);
}
.empty-state i {
font-size: 4rem;
color: var(--border-color);
margin-bottom: 20px;
}
.empty-state h3 {
font-size: 1.2rem;
color: var(--dark-gray);
margin-bottom: 10px;
}
.empty-state p {
font-size: 0.9rem;
margin-bottom: 20px;
}
/* Trial Expiry Panel */
.expiry-panel {
background-color: var(--white);
border-radius: 10px;
padding: 20px;
box-shadow: var(--shadow-sm);
margin-bottom: 20px;
}
.expiry-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
}
.expiry-panel-header h3 {
display: flex;
align-items: center;
gap: 10px;
font-size: 1rem;
color: var(--dark-gray);
margin: 0;
}
.expiry-panel-header h3 i {
color: var(--primary-orange);
}
.expiry-list {
max-height: 200px;
overflow-y: auto;
}
.expiry-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
border-radius: 8px;
margin-bottom: 8px;
background-color: var(--light-gray);
transition: all 0.2s ease;
}
.expiry-item:hover {
background-color: rgba(255, 107, 53, 0.1);
}
.expiry-item-info {
display: flex;
align-items: center;
gap: 10px;
}
.expiry-item-name {
font-weight: 600;
color: var(--dark-gray);
}
.expiry-countdown {
font-size: 0.8rem;
padding: 3px 8px;
border-radius: 10px;
}
.expiry-countdown.urgent {
background-color: #f8d7da;
color: #721c24;
}
.expiry-countdown.warning {
background-color: #fff3cd;
color: #856404;
}
.expiry-countdown.normal {
background-color: #cce5ff;
color: #004085;
}
/* Smart Reminder Banner */
.smart-reminder {
display: flex;
gap: 15px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.reminder-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 18px;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.reminder-item:hover {
transform: translateY(-2px);
}
.reminder-item.urgent {
background-color: #f8d7da;
color: #721c24;
}
.reminder-item.warning {
background-color: #fff3cd;
color: #856404;
}
.reminder-item i {
font-size: 1rem;
}
/* Stat Card Trend Indicator */
.stat-trend {
font-size: 0.8rem;
display: flex;
align-items: center;
gap: 4px;
margin-top: 5px;
}
.stat-trend.up {
color: #28a745;
}
.stat-trend.down {
color: #dc3545;
}
.stat-trend.neutral {
color: var(--medium-gray);
}
/* Table Row Enhancements */
.trial-row {
transition: all 0.2s ease;
}
.trial-row:hover {
background-color: rgba(255, 107, 53, 0.08) !important;
}
.action-cell {
white-space: nowrap;
position: sticky;
right: 0;
background-color: var(--white);
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.05);
}
tr:hover .action-cell {
background-color: rgba(255, 107, 53, 0.08);
}
/* Global Search Styles */
.global-search-container {
position: relative;
}
.search-box {
display: flex;
align-items: center;
background-color: var(--light-gray);
border-radius: 25px;
padding: 8px 15px;
width: 320px;
border: 2px solid transparent;
transition: all 0.3s ease;
}
.search-box:focus-within {
border-color: var(--primary-orange);
background-color: var(--white);
box-shadow: 0 4px 15px rgba(255, 107, 53, 0.15);
}
.search-shortcut {
background-color: rgba(0, 0, 0, 0.08);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.7rem;
color: var(--medium-gray);
font-family: system-ui;
margin-left: auto;
}
.search-results-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: var(--white);
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
margin-top: 8px;
max-height: 400px;
overflow-y: auto;
z-index: 2000;
}
.search-section-title {
padding: 12px 15px 8px;
font-size: 0.75rem;
font-weight: 600;
color: var(--medium-gray);
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 8px;
}
.search-section-title i {
font-size: 0.8rem;
}
.search-result-item,
.search-history-item {
padding: 12px 15px;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s ease;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.search-result-item:hover,
.search-history-item:hover {
background-color: rgba(255, 107, 53, 0.08);
}
.search-result-item i {
color: var(--primary-orange);
font-size: 1rem;
}
.search-result-info {
flex: 1;
}
.search-result-title {
font-weight: 600;
color: var(--dark-gray);
font-size: 0.9rem;
}
.search-result-meta {
font-size: 0.75rem;
color: var(--medium-gray);
margin-top: 2px;
}
.search-result-badge {
padding: 3px 8px;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 600;
}
.search-result-badge.trial {
background-color: #e3f2fd;
color: #1976d2;
}
.search-result-badge.progress {
background-color: #fff3e0;
color: #f57c00;
}
.search-result-badge.followup {
background-color: #e8f5e9;
color: #388e3c;
}
.search-empty {
padding: 20px;
text-align: center;
color: var(--medium-gray);
font-size: 0.9rem;
}
.search-history-item {
font-size: 0.85rem;
color: var(--dark-gray);
}
.search-history-item i {
color: var(--medium-gray);
}
/* Quick Add Dropdown Styles */
.quick-add-dropdown-container {
position: relative;
}
.quick-add-btn {
padding: 8px 16px;
font-size: 0.85rem;
}
.quick-add-btn .fa-caret-down {
margin-left: 4px;
font-size: 0.8rem;
}
.quick-add-dropdown {
position: absolute;
top: 100%;
right: 0;
background-color: var(--white);
border-radius: 10px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
margin-top: 8px;
min-width: 200px;
z-index: 2000;
overflow: hidden;
}
.quick-add-menu-item {
padding: 12px 18px;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s ease;
color: var(--dark-gray);
font-size: 0.9rem;
}
.quick-add-menu-item:hover {
background-color: rgba(255, 107, 53, 0.1);
}
.quick-add-menu-item i {
width: 20px;
text-align: center;
}
.quick-add-menu-item[data-action="trial"] i {
color: #17a2b8;
}
.quick-add-menu-item[data-action="customer"] i {
color: #28a745;
}
.quick-add-menu-item[data-action="followup"] i {
color: #ffc107;
}
/* Color System Variables Update */
:root {
--status-success: #27ae60;
--status-in-progress: #3498db;
--status-warning: #f39c12;
--status-error: #e74c3c;
--status-inactive: #95a5a6;
--brand-dark: #2c3e50;
}
/* Responsive Improvements */
@media (max-width: 1200px) {
.search-box {
width: 250px;
}
.quick-add-btn span {
display: none;
}
.quick-add-btn {
padding: 8px 12px;
}
}
@media (max-width: 992px) {
.search-box {
width: 200px;
}
.search-shortcut {
display: none;
}
}
@media (max-width: 768px) {
.global-search-container {
display: none;
}
.header-right {
gap: 10px;
}
.menu-toggle {
display: block;
}
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.main-content {
margin-left: 0;
}
}
/* Dashboard Cards Responsive */
.dashboard-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
@media (max-width: 1200px) {
.dashboard-stats {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 576px) {
.dashboard-stats {
grid-template-columns: 1fr;
}
}
/* Enhanced Status Colors */
.status-badge.status-active {
background-color: rgba(39, 174, 96, 0.15);
color: var(--status-success);
border-color: rgba(39, 174, 96, 0.3);
}
.status-badge.status-warning {
background-color: rgba(243, 156, 18, 0.15);
color: var(--status-warning);
border-color: rgba(243, 156, 18, 0.3);
}
.status-badge.status-expired,
.status-badge.status-urgent {
background-color: rgba(231, 76, 60, 0.15);
color: var(--status-error);
border-color: rgba(231, 76, 60, 0.3);
}
.status-badge.status-inactive {
background-color: rgba(149, 165, 166, 0.15);
color: var(--status-inactive);
border-color: rgba(149, 165, 166, 0.3);
}
.progress-badge.progress-completed {
background-color: rgba(39, 174, 96, 0.15);
color: var(--status-success);
}
.progress-badge.progress-in-progress {
background-color: rgba(52, 152, 219, 0.15);
color: var(--status-in-progress);
}
.progress-badge.progress-pending {
background-color: rgba(243, 156, 18, 0.15);
color: var(--status-warning);
}
.progress-badge.progress-rejected {
background-color: rgba(231, 76, 60, 0.15);
color: var(--status-error);
}
/* Keyboard Shortcut Indicator */
.keyboard-shortcut {
background-color: rgba(0, 0, 0, 0.08);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.7rem;
color: var(--medium-gray);
font-family: system-ui;
}

View File

@ -30,22 +30,37 @@
</div>
<nav class="sidebar-nav">
<a href="#" class="nav-item active" data-section="customer">
<i class="fas fa-user-plus"></i>
<span>客户信息</span>
</a>
<a href="#" class="nav-item" data-section="trialPeriods">
<i class="fas fa-clock"></i>
<span>客户试用时间</span>
</a>
<a href="#" class="nav-item" data-section="followup">
<i class="fas fa-tasks"></i>
<span>客户跟进</span>
</a>
<a href="#" class="nav-item" data-section="dashboard">
<i class="fas fa-chart-line"></i>
<span>数据仪表板</span>
</a>
<!-- 数据概览分组 -->
<div class="nav-group">
<div class="nav-group-title">
<i class="fas fa-chart-bar"></i>
<span>数据概览</span>
</div>
<a href="#" class="nav-item active" data-section="dashboard">
<i class="fas fa-chart-line"></i>
<span>数据仪表板</span>
</a>
</div>
<!-- 客户管理分组 -->
<div class="nav-group">
<div class="nav-group-title">
<i class="fas fa-users"></i>
<span>客户管理</span>
</div>
<a href="#" class="nav-item" data-section="trialPeriods">
<i class="fas fa-clock"></i>
<span>客户信息</span>
</a>
<a href="#" class="nav-item" data-section="customer">
<i class="fas fa-user-plus"></i>
<span>每周客户进度</span>
</a>
<a href="#" class="nav-item" data-section="followup">
<i class="fas fa-tasks"></i>
<span>客户跟进</span>
</a>
</div>
</nav>
<div class="sidebar-footer">
@ -64,26 +79,70 @@
<button class="menu-toggle" id="menuToggle">
<i class="fas fa-bars"></i>
</button>
<h1 id="pageTitle">客户信息</h1>
<h1 id="pageTitle">数据仪表板</h1>
</div>
<div class="header-right">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="customerSearchInput" placeholder="搜索客户...">
<!-- Global Search Box -->
<div class="global-search-container">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="globalSearchInput" placeholder="搜索客户、进度、跟进..." autocomplete="off">
<kbd class="search-shortcut">⌘K</kbd>
</div>
<!-- Search Results Dropdown -->
<div class="search-results-dropdown" id="searchResultsDropdown" style="display: none;">
<div class="search-history" id="searchHistory">
<div class="search-section-title"><i class="fas fa-history"></i> 搜索历史</div>
<div class="search-history-items" id="searchHistoryItems"></div>
</div>
<div class="search-results" id="searchResults">
<div class="search-section-title"><i class="fas fa-search"></i> 搜索结果</div>
<div class="search-results-items" id="searchResultsItems">
<div class="search-empty">输入关键词开始搜索...</div>
</div>
</div>
</div>
</div>
<button class="icon-btn">
<!-- Quick Add Button -->
<div class="quick-add-dropdown-container">
<button class="btn-primary quick-add-btn" id="headerQuickAddBtn">
<i class="fas fa-plus"></i>
<span>快速添加</span>
<i class="fas fa-caret-down"></i>
</button>
<div class="quick-add-dropdown" id="headerQuickAddDropdown" style="display: none;">
<div class="quick-add-menu-item" data-action="trial">
<i class="fas fa-clock"></i>
<span>添加试用客户</span>
</div>
<div class="quick-add-menu-item" data-action="customer">
<i class="fas fa-user-plus"></i>
<span>添加客户进度</span>
</div>
<div class="quick-add-menu-item" data-action="followup">
<i class="fas fa-tasks"></i>
<span>添加跟进记录</span>
</div>
</div>
</div>
<button class="icon-btn" title="通知">
<i class="fas fa-bell"></i>
</button>
<button class="icon-btn">
<button class="icon-btn" title="设置">
<i class="fas fa-cog"></i>
</button>
<button class="icon-btn" id="logoutBtn" title="退出登录">
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
</header>
<!-- Content Area -->
<main class="content-area">
<!-- Customer Management Section -->
<section id="customerSection" class="content-section active">
<section id="customerSection" class="content-section">
<div class="action-bar">
<button id="addCustomerBtn" class="btn-primary">
<i class="fas fa-plus"></i>
@ -200,19 +259,39 @@
<!-- Trial Periods Section -->
<section id="trialPeriodsSection" class="content-section">
<div class="section-title">
<h2><i class="fas fa-clock"></i> 客户试用时间</h2>
<h2><i class="fas fa-clock"></i> 客户信息</h2>
<button id="addTrialBtn" class="btn-primary">
<i class="fas fa-plus"></i>
添加试用时间
</button>
</div>
<!-- Filter Bar for Trial Periods -->
<div class="filter-bar">
<div class="filter-group">
<label for="trialStartDateFilter">开始日期:</label>
<input type="date" id="trialStartDateFilter" class="filter-input">
</div>
<div class="filter-group">
<label for="trialEndDateFilter">结束日期:</label>
<input type="date" id="trialEndDateFilter" class="filter-input">
</div>
<div class="filter-group">
<label for="trialSortOrder">排序:</label>
<select id="trialSortOrder" class="filter-select">
<option value="desc">结束时间倒序</option>
<option value="asc">结束时间顺序</option>
</select>
</div>
</div>
<div class="trial-periods-container">
<div class="table-container">
<table>
<thead>
<tr>
<th>客户名称</th>
<th>是否试用</th>
<th>开始时间</th>
<th>结束时间</th>
<th>创建时间</th>
@ -259,7 +338,7 @@
</section>
<!-- Dashboard Section -->
<section id="dashboardSection" class="content-section">
<section id="dashboardSection" class="content-section active">
<div class="dashboard-stats">
<div class="stat-card">
<div class="stat-icon">
@ -288,15 +367,6 @@
<p>已完成</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-users"></i>
</div>
<div class="stat-info">
<h3 id="totalProducts">0</h3>
<p>客户总数</p>
</div>
</div>
<div class="stat-card stat-card-highlight">
<div class="stat-icon">
<i class="fas fa-user-check"></i>
@ -381,6 +451,18 @@
<!-- Follow-up Section -->
<section id="followupSection" class="content-section">
<!-- Smart Reminder Banner -->
<div class="smart-reminder" id="followupReminder">
<div class="reminder-item warning" id="todayFollowupReminder" style="display: none;">
<i class="fas fa-exclamation-circle"></i>
<span>今日待跟进:<strong id="todayFollowupCount">0</strong> 个客户</span>
</div>
<div class="reminder-item urgent" id="overdueReminder" style="display: none;">
<i class="fas fa-bell"></i>
<span>逾期未跟进:<strong id="overdueCount">0</strong> 个客户</span>
</div>
</div>
<div class="action-bar">
<button id="addFollowUpBtn" class="btn-primary">
<i class="fas fa-plus"></i>
@ -528,7 +610,9 @@
</div>
<div class="form-group">
<label for="createCustomerName">客户名称</label>
<input type="text" id="createCustomerName" name="customerName" required>
<select id="createCustomerName" name="customerName" required>
<option value="">请选择客户</option>
</select>
</div>
</div>
<div class="form-row">
@ -710,11 +794,21 @@
<div class="modal-body">
<form id="addTrialPeriodForm">
<div class="form-group">
<label for="trialCustomerSelect">客户名称</label>
<select id="trialCustomerSelect" name="customerId" required>
<option value="">请选择客户</option>
<!-- Customer options will be loaded here -->
</select>
<label for="trialCustomerInput">客户名称</label>
<input type="text" id="trialCustomerInput" name="customerName" placeholder="请输入客户名称" required>
</div>
<div class="form-group">
<label>是否试用</label>
<div style="display: flex; gap: 20px; align-items: center;">
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="radio" name="isTrial" value="true" checked>
<span></span>
</label>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="radio" name="isTrial" value="false">
<span></span>
</label>
</div>
</div>
<div class="form-group">
<label for="trialStartTime">开始时间</label>

View File

@ -59,7 +59,7 @@ document.addEventListener('DOMContentLoaded', function () {
let trendChartInstance = null;
// Current section tracking
let currentSection = 'customer';
let currentSection = 'dashboard';
// Pagination state
let currentPage = 1;
@ -244,7 +244,8 @@ document.addEventListener('DOMContentLoaded', function () {
}
// Add Customer button
addCustomerBtn.addEventListener('click', function () {
addCustomerBtn.addEventListener('click', async function () {
await loadTrialCustomerListForCreate();
createModal.style.display = 'block';
});
@ -309,6 +310,29 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
// Load trial customer list for create customer dropdown
async function loadTrialCustomerListForCreate() {
try {
const response = await authenticatedFetch('/api/trial-customers/list');
const data = await response.json();
const customerSelect = document.getElementById('createCustomerName');
customerSelect.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;
customerSelect.appendChild(option);
});
}
} catch (error) {
console.error('Error loading trial customer list:', error);
}
}
// Type filter change event
const typeFilter = document.getElementById('typeFilter');
if (typeFilter) {
@ -384,7 +408,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (section === 'customer') {
customerSection.classList.add('active');
document.querySelector('[data-section="customer"]').classList.add('active');
pageTitle.textContent = '客户管理';
pageTitle.textContent = '每周客户进度';
currentSection = 'customer';
loadCustomers();
} else if (section === 'trialPeriods') {
@ -394,7 +418,7 @@ document.addEventListener('DOMContentLoaded', function () {
trialPeriodsSection.classList.add('active');
}
document.querySelector('[data-section="trialPeriods"]').classList.add('active');
pageTitle.textContent = '客户试用时间';
pageTitle.textContent = '客户信息';
currentSection = 'trialPeriods';
// Load customers first, then trial periods
@ -700,8 +724,27 @@ document.addEventListener('DOMContentLoaded', function () {
function renderCustomerTable(customers) {
customerTableBody.innerHTML = '';
// Helper function to generate status badge
function getStatusBadge(status) {
if (!status) return '';
const statusLower = status.toLowerCase();
if (status === '已完成' || statusLower.includes('完成') || statusLower.includes('complete')) {
return `<span class="progress-badge progress-completed"><i class="fas fa-check"></i> ${status}</span>`;
} else if (status === '进行中' || statusLower.includes('进行')) {
return `<span class="progress-badge progress-in-progress"><i class="fas fa-sync"></i> ${status}</span>`;
} else if (status === '待排期' || statusLower.includes('待')) {
return `<span class="progress-badge progress-pending"><i class="fas fa-clock"></i> ${status}</span>`;
} else if (status === '已驳回' || statusLower.includes('驳回')) {
return `<span class="progress-badge progress-rejected"><i class="fas fa-times"></i> ${status}</span>`;
} else if (status === '已上线' || statusLower.includes('上线')) {
return `<span class="progress-badge progress-launched"><i class="fas fa-rocket"></i> ${status}</span>`;
}
return `<span class="progress-badge">${status}</span>`;
}
customers.forEach(customer => {
const row = document.createElement('tr');
row.classList.add('customer-row');
const date = customer.intendedProduct || '';
@ -720,7 +763,16 @@ document.addEventListener('DOMContentLoaded', function () {
fields.forEach(field => {
const td = document.createElement('td');
const textValue = String(field.value ?? '');
td.textContent = textValue;
// Use badge for statusProgress field
if (field.name === 'statusProgress') {
td.innerHTML = getStatusBadge(textValue);
} else if (field.name === 'customerName') {
td.innerHTML = `<strong>${textValue}</strong>`;
} else {
td.textContent = textValue;
}
td.setAttribute('data-tooltip', textValue);
if (field.name === 'description' || field.name === 'solution') {
@ -731,6 +783,7 @@ document.addEventListener('DOMContentLoaded', function () {
});
const actionTd = document.createElement('td');
actionTd.classList.add('action-cell');
actionTd.innerHTML = `
<button class="action-btn edit-btn" data-id="${customer.id}">
<i class="fas fa-edit"></i>
@ -1019,9 +1072,51 @@ document.addEventListener('DOMContentLoaded', function () {
async function loadDashboardData() {
console.log('loadDashboardData called');
await loadAllCustomers();
await loadTrialCustomersForDashboard();
await loadFollowUpCount();
}
// Load trial customers for dashboard statistics
async function loadTrialCustomersForDashboard() {
try {
const response = await authenticatedFetch('/api/trial-periods/all');
const data = await response.json();
const trialPeriods = data.trialPeriods || [];
// Get customers map to resolve names
const customersResponse = await authenticatedFetch('/api/customers/list');
const customersData = await customersResponse.json();
const customersMap = customersData.customerMap || {};
// Collect unique trial customer names
const trialCustomerNames = new Set();
trialPeriods.forEach(period => {
// Check if customerId is in map, otherwise use customerId directly as name
const customerName = customersMap[period.customerId] || period.customerId;
if (customerName) {
trialCustomerNames.add(customerName);
}
});
// Update total customers to include trial customers
updateTotalCustomersWithTrialData(trialCustomerNames);
} catch (error) {
console.error('Error loading trial customers for dashboard:', error);
}
}
// Update total customers count including trial customers
function updateTotalCustomersWithTrialData(trialCustomerNames) {
// Get current dashboard customers
const progressCustomerNames = new Set(dashboardCustomers.map(c => c.customerName).filter(c => c));
// Merge both sets
const allCustomerNames = new Set([...progressCustomerNames, ...trialCustomerNames]);
// Update the total customers display
document.getElementById('totalCustomers').textContent = allCustomerNames.size;
}
// Update dashboard statistics
function updateDashboardStats(customers) {
const totalCustomers = new Set(customers.map(c => c.customerName).filter(c => c)).size;
@ -1040,9 +1135,6 @@ document.addEventListener('DOMContentLoaded', function () {
}).map(c => c.customerName).filter(c => c)
).size;
// 客户总数统计去重的客户名称数量与totalCustomers一致
const totalCustomersCount = new Set(customers.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'))
@ -1051,7 +1143,6 @@ document.addEventListener('DOMContentLoaded', function () {
document.getElementById('totalCustomers').textContent = totalCustomers;
document.getElementById('newCustomers').textContent = newCustomers;
document.getElementById('totalProducts').textContent = totalCustomersCount;
document.getElementById('completedTasks').textContent = completed;
}
@ -1500,7 +1591,7 @@ document.addEventListener('DOMContentLoaded', function () {
// Initialize the app
loadCustomers().then(() => {
loadAllCustomers();
loadDashboardData(); // Changed from loadAllCustomers() to include trial customers
});
// Pagination event listeners
@ -1562,23 +1653,6 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
// 登出功能
const logoutBtn = document.createElement('button');
logoutBtn.className = 'icon-btn';
logoutBtn.innerHTML = '<i class="fas fa-sign-out-alt"></i>';
logoutBtn.title = '登出';
logoutBtn.addEventListener('click', () => {
if (confirm('确定要退出登录吗?')) {
localStorage.removeItem('crmToken');
window.location.href = '/static/login.html';
}
});
const headerRight = document.querySelector('.header-right');
if (headerRight) {
headerRight.appendChild(logoutBtn);
}
// ========== Follow-up Management ==========
const followupSection = document.getElementById('followupSection');
const addFollowUpBtn = document.getElementById('addFollowUpBtn');
@ -1610,18 +1684,18 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
// Load customer list for follow-up dropdown
// Load customer list for follow-up dropdown from trial periods
async function loadCustomerListForFollowup() {
try {
const response = await authenticatedFetch('/api/customers/list');
const response = await authenticatedFetch('/api/trial-customers/list');
const data = await response.json();
followupCustomerNameSelect.innerHTML = '<option value="">请选择客户</option>';
if (data.customers && data.customers.length > 0) {
data.customers.forEach(customer => {
if (data.customerNames && data.customerNames.length > 0) {
data.customerNames.forEach(customerName => {
const option = document.createElement('option');
option.value = customer.id;
option.textContent = customer.customerName;
option.value = customerName;
option.textContent = customerName;
followupCustomerNameSelect.appendChild(option);
});
}
@ -1861,4 +1935,250 @@ document.addEventListener('DOMContentLoaded', function () {
loadFollowUps();
});
}
// ==========================================
// Global Search Functionality
// ==========================================
const globalSearchInput = document.getElementById('globalSearchInput');
const searchResultsDropdown = document.getElementById('searchResultsDropdown');
const searchResultsItems = document.getElementById('searchResultsItems');
const searchHistoryItems = document.getElementById('searchHistoryItems');
let searchHistory = JSON.parse(localStorage.getItem('crmSearchHistory') || '[]');
let searchDebounceTimer = null;
// Render search history
function renderSearchHistory() {
if (searchHistory.length === 0) {
searchHistoryItems.innerHTML = '<div class="search-empty">暂无搜索历史</div>';
return;
}
searchHistoryItems.innerHTML = searchHistory.slice(0, 5).map(item => `
<div class="search-history-item" data-query="${item}">
<i class="fas fa-history"></i>
<span>${item}</span>
</div>
`).join('');
// Add click handlers for history items
searchHistoryItems.querySelectorAll('.search-history-item').forEach(el => {
el.addEventListener('click', () => {
globalSearchInput.value = el.dataset.query;
performGlobalSearch(el.dataset.query);
});
});
}
// Save to search history
function saveToSearchHistory(query) {
if (!query.trim()) return;
searchHistory = [query, ...searchHistory.filter(h => h !== query)].slice(0, 10);
localStorage.setItem('crmSearchHistory', JSON.stringify(searchHistory));
renderSearchHistory();
}
// Perform global search
async function performGlobalSearch(query) {
if (!query.trim()) {
searchResultsItems.innerHTML = '<div class="search-empty">输入关键词开始搜索...</div>';
return;
}
searchResultsItems.innerHTML = '<div class="search-empty"><i class="fas fa-spinner fa-spin"></i> 搜索中...</div>';
try {
const results = [];
// Search in trial periods
const trialResponse = await authenticatedFetch('/api/trial-periods/all');
const trialData = await trialResponse.json();
const trialPeriods = trialData.trialPeriods || [];
// Get customers map
const customersResponse = await authenticatedFetch('/api/customers/list');
const customersData = await customersResponse.json();
const customersMap = customersData.customerMap || {};
// Search trial periods
trialPeriods.forEach(period => {
const customerName = customersMap[period.customerId] || period.customerId;
if (customerName.toLowerCase().includes(query.toLowerCase())) {
results.push({
type: 'trial',
title: customerName,
meta: `试用时间: ${period.startTime ? period.startTime.split('T')[0] : ''} ~ ${period.endTime ? period.endTime.split('T')[0] : ''}`,
icon: 'fas fa-clock',
section: 'trialPeriods'
});
}
});
// Search in customers (weekly progress)
const progressResponse = await authenticatedFetch('/api/customers?page=1&pageSize=100');
const progressData = await progressResponse.json();
const customers = progressData.customers || [];
customers.forEach(customer => {
const searchFields = [customer.customerName, customer.description, customer.solution, customer.module].join(' ').toLowerCase();
if (searchFields.includes(query.toLowerCase())) {
results.push({
type: 'progress',
title: customer.customerName,
meta: customer.description ? customer.description.substring(0, 50) + '...' : '',
icon: 'fas fa-user-plus',
section: 'customer'
});
}
});
// Search in followups
const followupResponse = await authenticatedFetch('/api/followups');
const followupData = await followupResponse.json();
const followups = followupData.followups || [];
followups.forEach(followup => {
if (followup.customerName.toLowerCase().includes(query.toLowerCase())) {
results.push({
type: 'followup',
title: followup.customerName,
meta: `状态: ${followup.dealStatus || '未知'} | 级别: ${followup.customerLevel || '未知'}`,
icon: 'fas fa-tasks',
section: 'followup'
});
}
});
// Render results
if (results.length === 0) {
searchResultsItems.innerHTML = '<div class="search-empty">未找到匹配结果</div>';
} else {
searchResultsItems.innerHTML = results.slice(0, 10).map(result => `
<div class="search-result-item" data-section="${result.section}">
<i class="${result.icon}"></i>
<div class="search-result-info">
<div class="search-result-title">${result.title}</div>
<div class="search-result-meta">${result.meta}</div>
</div>
<span class="search-result-badge ${result.type}">${result.type === 'trial' ? '试用' : result.type === 'progress' ? '进度' : '跟进'}</span>
</div>
`).join('');
// Add click handlers
searchResultsItems.querySelectorAll('.search-result-item').forEach(el => {
el.addEventListener('click', () => {
saveToSearchHistory(query);
switchSection(el.dataset.section);
searchResultsDropdown.style.display = 'none';
globalSearchInput.value = '';
});
});
}
} catch (error) {
console.error('Search error:', error);
searchResultsItems.innerHTML = '<div class="search-empty">搜索出错,请重试</div>';
}
}
// Search input event handlers
if (globalSearchInput) {
globalSearchInput.addEventListener('focus', () => {
renderSearchHistory();
searchResultsDropdown.style.display = 'block';
});
globalSearchInput.addEventListener('input', (e) => {
clearTimeout(searchDebounceTimer);
searchDebounceTimer = setTimeout(() => {
performGlobalSearch(e.target.value);
}, 300);
});
globalSearchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
searchResultsDropdown.style.display = 'none';
globalSearchInput.blur();
}
if (e.key === 'Enter') {
saveToSearchHistory(globalSearchInput.value);
}
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.global-search-container')) {
searchResultsDropdown.style.display = 'none';
}
});
// Keyboard shortcut (Cmd+K / Ctrl+K)
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
globalSearchInput.focus();
}
});
}
// ==========================================
// Header Quick Add Button
// ==========================================
const headerQuickAddBtn = document.getElementById('headerQuickAddBtn');
const headerQuickAddDropdown = document.getElementById('headerQuickAddDropdown');
if (headerQuickAddBtn) {
headerQuickAddBtn.addEventListener('click', (e) => {
e.stopPropagation();
const isVisible = headerQuickAddDropdown.style.display === 'block';
headerQuickAddDropdown.style.display = isVisible ? 'none' : 'block';
});
// Quick add menu item handlers
document.querySelectorAll('.quick-add-menu-item').forEach(item => {
item.addEventListener('click', () => {
const action = item.dataset.action;
headerQuickAddDropdown.style.display = 'none';
if (action === 'trial') {
switchSection('trialPeriods');
setTimeout(() => {
const addTrialBtn = document.getElementById('addTrialBtn');
if (addTrialBtn) addTrialBtn.click();
}, 100);
} else if (action === 'customer') {
switchSection('customer');
setTimeout(() => {
const addCustomerBtn = document.getElementById('addCustomerBtn');
if (addCustomerBtn) addCustomerBtn.click();
}, 100);
} else if (action === 'followup') {
switchSection('followup');
setTimeout(() => {
const addFollowUpBtn = document.getElementById('addFollowUpBtn');
if (addFollowUpBtn) addFollowUpBtn.click();
}, 100);
}
});
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.quick-add-dropdown-container')) {
headerQuickAddDropdown.style.display = 'none';
}
});
}
// ==========================================
// Logout Button
// ==========================================
const headerLogoutBtn = document.getElementById('logoutBtn');
if (headerLogoutBtn) {
headerLogoutBtn.addEventListener('click', () => {
if (confirm('确定要退出登录吗?')) {
localStorage.removeItem('authToken');
localStorage.removeItem('crmSearchHistory');
window.location.reload();
}
});
}
});

View File

@ -2,11 +2,15 @@
// 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() {
@ -25,42 +29,61 @@ function initTrialPeriodsPage() {
// Pagination controls
document.getElementById('trialFirstPage')?.addEventListener('click', () => {
trialCurrentPage = 1;
loadAllTrialPeriods();
applyTrialFiltersAndSort();
});
document.getElementById('trialPrevPage')?.addEventListener('click', () => {
if (trialCurrentPage > 1) {
trialCurrentPage--;
loadAllTrialPeriods();
applyTrialFiltersAndSort();
}
});
document.getElementById('trialNextPage')?.addEventListener('click', () => {
if (trialCurrentPage < trialTotalPages) {
trialCurrentPage++;
loadAllTrialPeriods();
applyTrialFiltersAndSort();
}
});
document.getElementById('trialLastPage')?.addEventListener('click', () => {
trialCurrentPage = trialTotalPages;
loadAllTrialPeriods();
applyTrialFiltersAndSort();
});
document.getElementById('trialPageSizeSelect')?.addEventListener('change', function () {
trialPageSize = parseInt(this.value);
trialCurrentPage = 1;
loadAllTrialPeriods();
applyTrialFiltersAndSort();
});
// Load customers for dropdown
loadCustomersForDropdown();
// 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 all customers for dropdown
async function loadCustomersForDropdown() {
// Load customers map for displaying customer names in table
async function loadCustomersMap() {
try {
console.log('Loading customers for dropdown...');
console.log('Loading customers map...');
const response = await authenticatedFetch('/api/customers/list');
if (!response) {
@ -71,36 +94,20 @@ async function loadCustomersForDropdown() {
const data = await response.json();
console.log('Customers data:', data);
const customers = data.customers || [];
// Use the complete customer map for ID->Name lookups
customersMap = data.customerMap || {};
console.log('Number of unique customers:', customers.length);
console.log('Number of customer ID mappings:', Object.keys(customersMap).length);
const select = document.getElementById('trialCustomerSelect');
if (!select) {
console.error('trialCustomerSelect element not found');
return;
}
// Keep the first option (请选择客户)
select.innerHTML = '<option value="">请选择客户</option>';
// Use the deduplicated list for dropdown options
customers.forEach(customer => {
const option = document.createElement('option');
option.value = customer.id;
option.textContent = customer.customerName;
select.appendChild(option);
});
console.log('Customers loaded successfully. Dropdown options:', customers.length);
} catch (error) {
console.error('Error loading customers:', error);
console.error('Error loading customers map:', error);
}
}
// 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() {
@ -109,19 +116,50 @@ async function loadAllTrialPeriods() {
const data = await response.json();
trialPeriodsData = data.trialPeriods || [];
trialTotalItems = trialPeriodsData.length;
trialTotalPages = Math.ceil(trialTotalItems / trialPageSize);
renderTrialPeriodsTable();
updateTrialPagination();
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');
@ -207,33 +245,65 @@ function renderTrialPeriodsTable() {
tbody.innerHTML = '';
if (trialPeriodsData.length === 0) {
if (filteredTrialPeriodsData.length === 0) {
const row = document.createElement('tr');
row.innerHTML = '<td colspan="5" style="text-align: center; padding: 30px; color: #999;">暂无试用时间记录</td>';
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, trialPeriodsData.length);
const pageData = trialPeriodsData.slice(startIndex, endIndex);
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');
// 显示客户名称如果找不到则显示ID
const customerName = customersMap[period.customerId] || period.customerId;
// 计算状态和到期天数
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>${customerName}</td>
<td><strong>${customerName}</strong></td>
<td>${statusBadge}</td>
<td>${startTime}</td>
<td>${endTime}</td>
<td>${createdAt}</td>
<td>
<td class="action-cell">
<button class="action-btn edit-btn" data-id="${period.id}" title="编辑">
<i class="fas fa-edit"></i>
</button>
@ -296,7 +366,7 @@ function updateTrialPagination() {
pageBtn.textContent = i;
pageBtn.addEventListener('click', () => {
trialCurrentPage = i;
loadAllTrialPeriods();
applyTrialFiltersAndSort();
});
pageNumbers.appendChild(pageBtn);
}
@ -305,20 +375,16 @@ function updateTrialPagination() {
// Open add trial modal
function openAddTrialModal() {
console.log('Opening add trial modal');
// Load customers first
loadCustomersForDropdown().then(() => {
console.log('Customers loaded, opening modal');
document.getElementById('trialCustomerSelect').value = '';
document.getElementById('trialStartTime').value = '';
document.getElementById('trialEndTime').value = '';
document.getElementById('addTrialPeriodModal').style.display = 'block';
});
document.getElementById('trialCustomerInput').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 = trialPeriodsData.find(p => p.id === periodId);
const period = filteredTrialPeriodsData.find(p => p.id === periodId);
if (!period) return;
document.getElementById('editTrialPeriodId').value = period.id;
@ -357,12 +423,14 @@ async function deleteTrialPeriodFromPage(periodId) {
// Create trial period from page
async function createTrialPeriodFromPage() {
const customerId = document.getElementById('trialCustomerSelect').value;
const customerName = document.getElementById('trialCustomerInput').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 (!customerId) {
alert('请选择客户');
if (!customerName) {
alert('请输入客户名称');
return;
}
@ -371,10 +439,25 @@ async function createTrialPeriodFromPage() {
return;
}
// Find or create customer ID from customer name
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 = {
customerId: customerId,
startTime: new Date(startTime).toISOString(),
endTime: new Date(endTime).toISOString()
endTime: new Date(endTime).toISOString(),
isTrial: isTrial
};
try {

View File

@ -269,8 +269,6 @@ func (h *FollowUpHandler) GetCustomerList(w http.ResponseWriter, r *http.Request
return
}
fmt.Printf("DEBUG: Total customers from storage: %d\n", len(customers))
// Create a list of customer objects with id and name
type CustomerInfo struct {
ID string `json:"id"`
@ -295,14 +293,10 @@ func (h *FollowUpHandler) GetCustomerList(w http.ResponseWriter, r *http.Request
CustomerName: customer.CustomerName,
})
seenNames[customer.CustomerName] = true
fmt.Printf("DEBUG: Added customer: ID=%s, Name=%s\n", customer.ID, customer.CustomerName)
}
}
}
fmt.Printf("DEBUG: Total unique customer list items: %d\n", len(customerList))
fmt.Printf("DEBUG: Total customer ID mappings: %d\n", len(customerMap))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"customers": customerList, // Deduplicated list for dropdown

View File

@ -86,6 +86,7 @@ func (h *TrialPeriodHandler) CreateTrialPeriod(w http.ResponseWriter, r *http.Re
CustomerID: req.CustomerID,
StartTime: startTime,
EndTime: endTime,
IsTrial: req.IsTrial,
CreatedAt: time.Now(),
}
@ -311,8 +312,6 @@ func (h *TrialPeriodHandler) sendTrialNotification(customerID string, startTime,
return
}
log.Printf("DEBUG: Sending Feishu message from trial_period_handler: %s", string(jsonData))
resp, err := http.Post(h.feishuWebhook, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
log.Printf("Failed to send Feishu notification: %v", err)
@ -327,3 +326,47 @@ func (h *TrialPeriodHandler) sendTrialNotification(customerID string, startTime,
log.Printf("Sent trial notification for customer: %s (expires in %d days)", customerID, daysUntilExpiry)
}
// GetTrialCustomerList returns a list of unique customer names from trial periods
func (h *TrialPeriodHandler) GetTrialCustomerList(w http.ResponseWriter, r *http.Request) {
trialPeriods, err := h.storage.GetAllTrialPeriods()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Create a set of unique customer IDs
customerIDs := make(map[string]bool)
for _, period := range trialPeriods {
if period.CustomerID != "" {
customerIDs[period.CustomerID] = true
}
}
// Get unique customer names
seenNames := make(map[string]bool)
var customerNames []string
for customerID := range customerIDs {
// Try to get customer name from customer storage
var customerName string
customer, err := h.customerStorage.GetCustomerByID(customerID)
if err == nil && customer != nil && customer.CustomerName != "" {
customerName = customer.CustomerName
} else {
// If customer not found, use customer ID as name
customerName = customerID
}
// Add to list if not seen before
if !seenNames[customerName] {
customerNames = append(customerNames, customerName)
seenNames[customerName] = true
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"customerNames": customerNames,
})
}

View File

@ -8,6 +8,7 @@ type TrialPeriod struct {
CustomerID string `json:"customerId"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
IsTrial bool `json:"isTrial"`
CreatedAt time.Time `json:"createdAt"`
}
@ -16,6 +17,7 @@ type CreateTrialPeriodRequest struct {
CustomerID string `json:"customerId"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
IsTrial bool `json:"isTrial"`
}
// UpdateTrialPeriodRequest represents the request to update a trial period