feat_ui
This commit is contained in:
parent
569ca8479f
commit
9952f1569e
@ -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
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user