feat_data
This commit is contained in:
parent
36141679d3
commit
c1d1be8e27
@ -615,6 +615,51 @@ body {
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Checkbox Group Styles */
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
background-color: var(--white);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label:hover {
|
||||||
|
border-color: var(--primary-orange);
|
||||||
|
background-color: rgba(255, 107, 53, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type="checkbox"] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: var(--primary-orange);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type="checkbox"]:checked+span {
|
||||||
|
color: var(--primary-orange);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label:has(input[type="checkbox"]:checked) {
|
||||||
|
border-color: var(--primary-orange);
|
||||||
|
background-color: rgba(255, 107, 53, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* File Upload */
|
/* File Upload */
|
||||||
.file-upload {
|
.file-upload {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@ -298,7 +298,7 @@
|
|||||||
<div class="section-title" style="justify-content: flex-end;">
|
<div class="section-title" style="justify-content: flex-end;">
|
||||||
<button id="addTrialBtn" class="btn-primary">
|
<button id="addTrialBtn" class="btn-primary">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
添加试用时间
|
添加客户
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -328,6 +328,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>客户名称</th>
|
<th>客户名称</th>
|
||||||
<th>来源</th>
|
<th>来源</th>
|
||||||
|
<th>意向产品</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>开始时间</th>
|
<th>开始时间</th>
|
||||||
<th>结束时间</th>
|
<th>结束时间</th>
|
||||||
@ -438,8 +439,6 @@
|
|||||||
<option value="type">类型</option>
|
<option value="type">类型</option>
|
||||||
<option value="module">模块</option>
|
<option value="module">模块</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" id="statusChartTitle" class="chart-title-input"
|
|
||||||
placeholder="自定义标题" value="数据分布">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -455,8 +454,6 @@
|
|||||||
<option value="type">类型</option>
|
<option value="type">类型</option>
|
||||||
<option value="module">模块</option>
|
<option value="module">模块</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" id="typeChartTitle" class="chart-title-input" placeholder="自定义标题"
|
|
||||||
value="客户类型">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -681,7 +678,10 @@
|
|||||||
<option value="模型工坊">模型工坊</option>
|
<option value="模型工坊">模型工坊</option>
|
||||||
<option value="模型广场">模型广场</option>
|
<option value="模型广场">模型广场</option>
|
||||||
<option value="镜像管理">镜像管理</option>
|
<option value="镜像管理">镜像管理</option>
|
||||||
|
<option value="其他">其他</option>
|
||||||
</select>
|
</select>
|
||||||
|
<input type="text" id="createModuleOther" name="moduleOther" placeholder="请输入其他模块名称"
|
||||||
|
style="display: none; margin-top: 8px;">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="createStatusProgress">状态与进度</label>
|
<label for="createStatusProgress">状态与进度</label>
|
||||||
@ -792,7 +792,10 @@
|
|||||||
<option value="模型工坊">模型工坊</option>
|
<option value="模型工坊">模型工坊</option>
|
||||||
<option value="模型广场">模型广场</option>
|
<option value="模型广场">模型广场</option>
|
||||||
<option value="镜像管理">镜像管理</option>
|
<option value="镜像管理">镜像管理</option>
|
||||||
|
<option value="其他">其他</option>
|
||||||
</select>
|
</select>
|
||||||
|
<input type="text" id="editModuleOther" name="moduleOther" placeholder="请输入其他模块名称"
|
||||||
|
style="display: none; margin-top: 8px;">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editStatusProgress">状态与进度</label>
|
<label for="editStatusProgress">状态与进度</label>
|
||||||
@ -825,7 +828,7 @@
|
|||||||
<div id="addTrialPeriodModal" class="modal">
|
<div id="addTrialPeriodModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3><i class="fas fa-clock"></i> 添加试用时间</h3>
|
<h3><i class="fas fa-user-plus"></i> 添加客户</h3>
|
||||||
<span class="close">×</span>
|
<span class="close">×</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@ -838,6 +841,26 @@
|
|||||||
<label for="trialCustomerSource">客户来源</label>
|
<label for="trialCustomerSource">客户来源</label>
|
||||||
<input type="text" id="trialCustomerSource" name="source" placeholder="请输入客户来源(如:官网、展会、推荐等)">
|
<input type="text" id="trialCustomerSource" name="source" placeholder="请输入客户来源(如:官网、展会、推荐等)">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>意向产品(可多选)</label>
|
||||||
|
<div class="checkbox-group" id="trialIntendedProductGroup">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="intendedProduct" value="数据闭环">
|
||||||
|
<span>数据闭环</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="intendedProduct" value="robogo">
|
||||||
|
<span>robogo</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="intendedProduct" value="其他"
|
||||||
|
id="trialIntendedProductOtherCheckbox">
|
||||||
|
<span>其他</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="trialIntendedProductOther" name="intendedProductOther"
|
||||||
|
placeholder="请输入其他意向产品" style="display: none; margin-top: 8px;">
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>是否试用</label>
|
<label>是否试用</label>
|
||||||
<div style="display: flex; gap: 20px; align-items: center;">
|
<div style="display: flex; gap: 20px; align-items: center;">
|
||||||
@ -888,6 +911,26 @@
|
|||||||
<label for="editTrialCustomerSource">客户来源</label>
|
<label for="editTrialCustomerSource">客户来源</label>
|
||||||
<input type="text" id="editTrialCustomerSource" name="source" placeholder="请输入客户来源:如社区、销售、推荐等">
|
<input type="text" id="editTrialCustomerSource" name="source" placeholder="请输入客户来源:如社区、销售、推荐等">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>意向产品(可多选)</label>
|
||||||
|
<div class="checkbox-group" id="editTrialIntendedProductGroup">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="editIntendedProduct" value="数据闭环">
|
||||||
|
<span>数据闭环</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="editIntendedProduct" value="robogo">
|
||||||
|
<span>robogo</span>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="editIntendedProduct" value="其他"
|
||||||
|
id="editTrialIntendedProductOtherCheckbox">
|
||||||
|
<span>其他</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="editTrialIntendedProductOther" name="intendedProductOther"
|
||||||
|
placeholder="请输入其他意向产品" style="display: none; margin-top: 8px;">
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>是否试用</label>
|
<label>是否试用</label>
|
||||||
<div style="display: flex; gap: 20px; align-items: center;">
|
<div style="display: flex; gap: 20px; align-items: center;">
|
||||||
@ -1044,9 +1087,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script src="/static/js/trial-periods.js?v=1.2"></script>
|
<script src="/static/js/trial-periods.js?v=1.3"></script>
|
||||||
<script src="/static/js/trial-periods-page.js?v=1.2"></script>
|
<script src="/static/js/trial-periods-page.js?v=1.3"></script>
|
||||||
<script src="/static/js/main.js?v=1.2"></script>
|
<script src="/static/js/main.js?v=1.4"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@ -228,6 +228,72 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
bindCellTooltipEvents();
|
bindCellTooltipEvents();
|
||||||
|
|
||||||
|
// Setup module dropdown with "other" option handling
|
||||||
|
function setupModuleDropdown(selectId, otherInputId) {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
const otherInput = document.getElementById(otherInputId);
|
||||||
|
|
||||||
|
if (!select || !otherInput) return;
|
||||||
|
|
||||||
|
select.addEventListener('change', function () {
|
||||||
|
if (this.value === '其他') {
|
||||||
|
otherInput.style.display = 'block';
|
||||||
|
otherInput.focus();
|
||||||
|
} else {
|
||||||
|
otherInput.style.display = 'none';
|
||||||
|
otherInput.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get module value (handles "other" option)
|
||||||
|
function getModuleValue(selectId, otherInputId) {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
const otherInput = document.getElementById(otherInputId);
|
||||||
|
|
||||||
|
if (!select) return '';
|
||||||
|
|
||||||
|
if (select.value === '其他' && otherInput && otherInput.value.trim()) {
|
||||||
|
return otherInput.value.trim();
|
||||||
|
}
|
||||||
|
return select.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set module value in form (handles "other" option)
|
||||||
|
function setModuleValue(selectId, otherInputId, value) {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
const otherInput = document.getElementById(otherInputId);
|
||||||
|
|
||||||
|
if (!select) return;
|
||||||
|
|
||||||
|
// Check if value matches one of the predefined options
|
||||||
|
const predefinedOptions = ['数据生成', '数据集', '数据空间', '模型工坊', '模型广场', '镜像管理', ''];
|
||||||
|
if (predefinedOptions.includes(value)) {
|
||||||
|
select.value = value;
|
||||||
|
if (otherInput) {
|
||||||
|
otherInput.style.display = 'none';
|
||||||
|
otherInput.value = '';
|
||||||
|
}
|
||||||
|
} else if (value) {
|
||||||
|
// Custom value - select "other" and fill input
|
||||||
|
select.value = '其他';
|
||||||
|
if (otherInput) {
|
||||||
|
otherInput.style.display = 'block';
|
||||||
|
otherInput.value = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
select.value = '';
|
||||||
|
if (otherInput) {
|
||||||
|
otherInput.style.display = 'none';
|
||||||
|
otherInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize module dropdowns
|
||||||
|
setupModuleDropdown('createModule', 'createModuleOther');
|
||||||
|
setupModuleDropdown('editModule', 'editModuleOther');
|
||||||
|
|
||||||
function normalizeDateValue(dateValue) {
|
function normalizeDateValue(dateValue) {
|
||||||
const raw = String(dateValue || '').trim();
|
const raw = String(dateValue || '').trim();
|
||||||
if (!raw) return '';
|
if (!raw) return '';
|
||||||
@ -932,7 +998,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
description: document.getElementById('createDescription').value,
|
description: document.getElementById('createDescription').value,
|
||||||
solution: document.getElementById('createSolution').value,
|
solution: document.getElementById('createSolution').value,
|
||||||
type: document.getElementById('createType').value,
|
type: document.getElementById('createType').value,
|
||||||
module: document.getElementById('createModule').value,
|
module: getModuleValue('createModule', 'createModuleOther'),
|
||||||
statusProgress: document.getElementById('createStatusProgress').value,
|
statusProgress: document.getElementById('createStatusProgress').value,
|
||||||
reporter: '' // Trial periods managed separately
|
reporter: '' // Trial periods managed separately
|
||||||
};
|
};
|
||||||
@ -948,9 +1014,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
createCustomerForm.reset();
|
createCustomerForm.reset();
|
||||||
|
// Hide module "other" input
|
||||||
|
const createModuleOther = document.getElementById('createModuleOther');
|
||||||
|
if (createModuleOther) createModuleOther.style.display = 'none';
|
||||||
createModal.style.display = 'none';
|
createModal.style.display = 'none';
|
||||||
loadCustomers();
|
loadCustomers();
|
||||||
alert('客户创建成功!');
|
|
||||||
} else {
|
} else {
|
||||||
alert('创建客户时出错');
|
alert('创建客户时出错');
|
||||||
}
|
}
|
||||||
@ -1010,7 +1078,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
document.getElementById('editDescription').value = customer.description || '';
|
document.getElementById('editDescription').value = customer.description || '';
|
||||||
document.getElementById('editSolution').value = customer.solution || '';
|
document.getElementById('editSolution').value = customer.solution || '';
|
||||||
document.getElementById('editType').value = customer.type || '';
|
document.getElementById('editType').value = customer.type || '';
|
||||||
document.getElementById('editModule').value = customer.module || '';
|
setModuleValue('editModule', 'editModuleOther', customer.module || '');
|
||||||
document.getElementById('editStatusProgress').value = customer.statusProgress || '';
|
document.getElementById('editStatusProgress').value = customer.statusProgress || '';
|
||||||
|
|
||||||
// Parse trial period and fill datetime inputs
|
// Parse trial period and fill datetime inputs
|
||||||
@ -1071,7 +1139,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
description: document.getElementById('editDescription').value,
|
description: document.getElementById('editDescription').value,
|
||||||
solution: document.getElementById('editSolution').value,
|
solution: document.getElementById('editSolution').value,
|
||||||
type: document.getElementById('editType').value,
|
type: document.getElementById('editType').value,
|
||||||
module: document.getElementById('editModule').value,
|
module: getModuleValue('editModule', 'editModuleOther'),
|
||||||
statusProgress: document.getElementById('editStatusProgress').value,
|
statusProgress: document.getElementById('editStatusProgress').value,
|
||||||
reporter: '' // Trial periods managed separately
|
reporter: '' // Trial periods managed separately
|
||||||
};
|
};
|
||||||
@ -1088,7 +1156,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
editModal.style.display = 'none';
|
editModal.style.display = 'none';
|
||||||
loadCustomers();
|
loadCustomers();
|
||||||
alert('客户更新成功!');
|
|
||||||
} else {
|
} else {
|
||||||
alert('更新客户时出错');
|
alert('更新客户时出错');
|
||||||
}
|
}
|
||||||
@ -1111,7 +1178,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
loadCustomers();
|
loadCustomers();
|
||||||
alert('客户删除成功!');
|
|
||||||
} else {
|
} else {
|
||||||
alert('删除客户时出错');
|
alert('删除客户时出错');
|
||||||
}
|
}
|
||||||
@ -1817,7 +1883,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
followupForm.reset();
|
followupForm.reset();
|
||||||
followupFormCard.style.display = 'none';
|
followupFormCard.style.display = 'none';
|
||||||
loadFollowUps();
|
loadFollowUps();
|
||||||
alert('跟进记录创建成功!');
|
|
||||||
} else {
|
} else {
|
||||||
alert('创建跟进记录时出错');
|
alert('创建跟进记录时出错');
|
||||||
}
|
}
|
||||||
@ -1928,7 +1993,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
loadFollowUps();
|
loadFollowUps();
|
||||||
alert('跟进记录删除成功!');
|
|
||||||
} else {
|
} else {
|
||||||
alert('删除跟进记录时出错');
|
alert('删除跟进记录时出错');
|
||||||
}
|
}
|
||||||
@ -1971,7 +2035,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
addFollowupModalForm.reset();
|
addFollowupModalForm.reset();
|
||||||
addFollowupModal.style.display = 'none';
|
addFollowupModal.style.display = 'none';
|
||||||
loadFollowUps();
|
loadFollowUps();
|
||||||
alert('跟进记录创建成功!');
|
|
||||||
} else {
|
} else {
|
||||||
alert('创建跟进记录时出错');
|
alert('创建跟进记录时出错');
|
||||||
}
|
}
|
||||||
@ -2049,7 +2112,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
editFollowupForm.reset();
|
editFollowupForm.reset();
|
||||||
editFollowupModal.style.display = 'none';
|
editFollowupModal.style.display = 'none';
|
||||||
loadFollowUps();
|
loadFollowUps();
|
||||||
alert('跟进记录更新成功!');
|
|
||||||
} else {
|
} else {
|
||||||
alert('更新跟进记录时出错');
|
alert('更新跟进记录时出错');
|
||||||
}
|
}
|
||||||
@ -2222,19 +2284,21 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
// Search in trial periods
|
// Search in trial periods
|
||||||
|
try {
|
||||||
const trialResponse = await authenticatedFetch('/api/trial-periods/all');
|
const trialResponse = await authenticatedFetch('/api/trial-periods/all');
|
||||||
|
if (trialResponse.ok) {
|
||||||
const trialData = await trialResponse.json();
|
const trialData = await trialResponse.json();
|
||||||
const trialPeriods = trialData.trialPeriods || [];
|
const trialPeriods = trialData.trialPeriods || [];
|
||||||
|
|
||||||
// Get customers map
|
// Get customers map
|
||||||
const customersResponse = await authenticatedFetch('/api/customers/list');
|
const customersResponse = await authenticatedFetch('/api/customers/list');
|
||||||
const customersData = await customersResponse.json();
|
const customersData = customersResponse.ok ? await customersResponse.json() : {};
|
||||||
const customersMap = customersData.customerMap || {};
|
const customersMap = customersData.customerMap || {};
|
||||||
|
|
||||||
// Search trial periods
|
// Search trial periods
|
||||||
trialPeriods.forEach(period => {
|
trialPeriods.forEach(period => {
|
||||||
const customerName = customersMap[period.customerId] || period.customerId;
|
const customerName = customersMap[period.customerId] || period.customerId || '';
|
||||||
if (customerName.toLowerCase().includes(query.toLowerCase())) {
|
if (customerName && customerName.toLowerCase().includes(query.toLowerCase())) {
|
||||||
results.push({
|
results.push({
|
||||||
type: 'trial',
|
type: 'trial',
|
||||||
title: customerName,
|
title: customerName,
|
||||||
@ -2244,32 +2308,69 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
} catch (trialError) {
|
||||||
|
console.warn('Trial periods search error:', trialError);
|
||||||
|
}
|
||||||
|
|
||||||
// Search in customers (weekly progress)
|
// Search in customers (weekly progress) - use local allCustomers if available
|
||||||
const progressResponse = await authenticatedFetch('/api/customers?page=1&pageSize=100');
|
try {
|
||||||
const progressData = await progressResponse.json();
|
if (typeof allCustomers !== 'undefined' && allCustomers.length > 0) {
|
||||||
const customers = progressData.customers || [];
|
allCustomers.forEach(customer => {
|
||||||
|
const searchFields = [
|
||||||
customers.forEach(customer => {
|
customer.customerName || '',
|
||||||
const searchFields = [customer.customerName, customer.description, customer.solution, customer.module].join(' ').toLowerCase();
|
customer.description || '',
|
||||||
|
customer.solution || '',
|
||||||
|
customer.module || ''
|
||||||
|
].join(' ').toLowerCase();
|
||||||
if (searchFields.includes(query.toLowerCase())) {
|
if (searchFields.includes(query.toLowerCase())) {
|
||||||
results.push({
|
results.push({
|
||||||
type: 'progress',
|
type: 'progress',
|
||||||
title: customer.customerName,
|
title: customer.customerName || '未知客户',
|
||||||
meta: customer.description ? customer.description.substring(0, 50) + '...' : '',
|
meta: customer.description ? customer.description.substring(0, 50) + '...' : '',
|
||||||
icon: 'fas fa-user-plus',
|
icon: 'fas fa-user-plus',
|
||||||
section: 'customer'
|
section: 'customer'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
const progressResponse = await authenticatedFetch('/api/customers?page=1&pageSize=100');
|
||||||
|
if (progressResponse.ok) {
|
||||||
|
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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (progressError) {
|
||||||
|
console.warn('Progress search error:', progressError);
|
||||||
|
}
|
||||||
|
|
||||||
// Search in followups
|
// Search in followups
|
||||||
|
try {
|
||||||
const followupResponse = await authenticatedFetch('/api/followups');
|
const followupResponse = await authenticatedFetch('/api/followups');
|
||||||
|
if (followupResponse.ok) {
|
||||||
const followupData = await followupResponse.json();
|
const followupData = await followupResponse.json();
|
||||||
const followups = followupData.followups || [];
|
const followups = followupData.followups || [];
|
||||||
|
|
||||||
followups.forEach(followup => {
|
followups.forEach(followup => {
|
||||||
if (followup.customerName.toLowerCase().includes(query.toLowerCase())) {
|
if (followup.customerName && followup.customerName.toLowerCase().includes(query.toLowerCase())) {
|
||||||
results.push({
|
results.push({
|
||||||
type: 'followup',
|
type: 'followup',
|
||||||
title: followup.customerName,
|
title: followup.customerName,
|
||||||
@ -2279,12 +2380,25 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
} catch (followupError) {
|
||||||
|
console.warn('Followup search error:', followupError);
|
||||||
|
}
|
||||||
|
|
||||||
// Render results
|
// Render results
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
searchResultsItems.innerHTML = '<div class="search-empty">未找到匹配结果</div>';
|
searchResultsItems.innerHTML = '<div class="search-empty">未找到匹配结果</div>';
|
||||||
} else {
|
} else {
|
||||||
searchResultsItems.innerHTML = results.slice(0, 10).map(result => `
|
// Deduplicate by title
|
||||||
|
const seen = new Set();
|
||||||
|
const uniqueResults = results.filter(r => {
|
||||||
|
const key = r.title + r.type;
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
searchResultsItems.innerHTML = uniqueResults.slice(0, 10).map(result => `
|
||||||
<div class="search-result-item" data-section="${result.section}">
|
<div class="search-result-item" data-section="${result.section}">
|
||||||
<i class="${result.icon}"></i>
|
<i class="${result.icon}"></i>
|
||||||
<div class="search-result-info">
|
<div class="search-result-info">
|
||||||
|
|||||||
@ -76,6 +76,10 @@ function initTrialPeriodsPage() {
|
|||||||
applyTrialFiltersAndSort();
|
applyTrialFiltersAndSort();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Intended product checkbox change handlers
|
||||||
|
setupIntendedProductCheckboxes('trialIntendedProductOtherCheckbox', 'trialIntendedProductOther');
|
||||||
|
setupIntendedProductCheckboxes('editTrialIntendedProductOtherCheckbox', 'editTrialIntendedProductOther');
|
||||||
|
|
||||||
// Load customers map for displaying customer names
|
// Load customers map for displaying customer names
|
||||||
loadCustomersMap();
|
loadCustomersMap();
|
||||||
}
|
}
|
||||||
@ -133,6 +137,120 @@ async function loadCustomersForDropdown() {
|
|||||||
return await loadCustomersMap();
|
return await loadCustomersMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup intended product checkboxes with "other" option handling
|
||||||
|
function setupIntendedProductCheckboxes(otherCheckboxId, otherInputId) {
|
||||||
|
const otherCheckbox = document.getElementById(otherCheckboxId);
|
||||||
|
const otherInput = document.getElementById(otherInputId);
|
||||||
|
|
||||||
|
if (!otherCheckbox || !otherInput) return;
|
||||||
|
|
||||||
|
otherCheckbox.addEventListener('change', function () {
|
||||||
|
if (this.checked) {
|
||||||
|
otherInput.style.display = 'block';
|
||||||
|
otherInput.focus();
|
||||||
|
} else {
|
||||||
|
otherInput.style.display = 'none';
|
||||||
|
otherInput.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get intended product values from checkboxes (returns comma-separated string)
|
||||||
|
function getIntendedProductValues(checkboxName, otherInputId) {
|
||||||
|
const checkboxes = document.querySelectorAll(`input[name="${checkboxName}"]:checked`);
|
||||||
|
const otherInput = document.getElementById(otherInputId);
|
||||||
|
|
||||||
|
const values = [];
|
||||||
|
checkboxes.forEach(cb => {
|
||||||
|
if (cb.value === '其他') {
|
||||||
|
// If "other" is checked, use the custom input value
|
||||||
|
if (otherInput && otherInput.value.trim()) {
|
||||||
|
values.push(otherInput.value.trim());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
values.push(cb.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return values.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set intended product values in checkboxes (accepts comma-separated string)
|
||||||
|
function setIntendedProductValues(checkboxName, otherInputId, value) {
|
||||||
|
const checkboxes = document.querySelectorAll(`input[name="${checkboxName}"]`);
|
||||||
|
const otherInput = document.getElementById(otherInputId);
|
||||||
|
|
||||||
|
// Reset all checkboxes
|
||||||
|
checkboxes.forEach(cb => {
|
||||||
|
cb.checked = false;
|
||||||
|
});
|
||||||
|
if (otherInput) {
|
||||||
|
otherInput.style.display = 'none';
|
||||||
|
otherInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
// Parse comma-separated values
|
||||||
|
const selectedValues = value.split(',').map(v => v.trim()).filter(v => v);
|
||||||
|
const predefinedOptions = ['数据闭环', 'robogo'];
|
||||||
|
|
||||||
|
let hasOther = false;
|
||||||
|
const otherValues = [];
|
||||||
|
|
||||||
|
selectedValues.forEach(val => {
|
||||||
|
if (predefinedOptions.includes(val)) {
|
||||||
|
// Check the corresponding checkbox
|
||||||
|
checkboxes.forEach(cb => {
|
||||||
|
if (cb.value === val) {
|
||||||
|
cb.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Custom value - mark as "other"
|
||||||
|
hasOther = true;
|
||||||
|
otherValues.push(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle "other" checkbox and input
|
||||||
|
if (hasOther) {
|
||||||
|
checkboxes.forEach(cb => {
|
||||||
|
if (cb.value === '其他') {
|
||||||
|
cb.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (otherInput) {
|
||||||
|
otherInput.style.display = 'block';
|
||||||
|
otherInput.value = otherValues.join(', ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy compatibility functions (for backward compatibility)
|
||||||
|
function setupIntendedProductDropdown(selectId, otherId) {
|
||||||
|
// No-op for backward compatibility - now using checkbox groups
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIntendedProductValue(selectId, otherId) {
|
||||||
|
// Map to new checkbox-based function based on the select ID
|
||||||
|
if (selectId === 'trialIntendedProduct') {
|
||||||
|
return getIntendedProductValues('intendedProduct', 'trialIntendedProductOther');
|
||||||
|
} else if (selectId === 'editTrialIntendedProduct') {
|
||||||
|
return getIntendedProductValues('editIntendedProduct', 'editTrialIntendedProductOther');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setIntendedProductValue(selectId, otherId, value) {
|
||||||
|
// Map to new checkbox-based function based on the select ID
|
||||||
|
if (selectId === 'trialIntendedProduct') {
|
||||||
|
setIntendedProductValues('intendedProduct', 'trialIntendedProductOther', value);
|
||||||
|
} else if (selectId === 'editTrialIntendedProduct') {
|
||||||
|
setIntendedProductValues('editIntendedProduct', 'editTrialIntendedProductOther', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Load all trial periods
|
// Load all trial periods
|
||||||
async function loadAllTrialPeriods() {
|
async function loadAllTrialPeriods() {
|
||||||
@ -274,10 +392,10 @@ function renderTrialPeriodsTable() {
|
|||||||
if (filteredTrialPeriodsData.length === 0) {
|
if (filteredTrialPeriodsData.length === 0) {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td colspan="7" class="empty-state">
|
<td colspan="8" class="empty-state">
|
||||||
<i class="fas fa-users"></i>
|
<i class="fas fa-users"></i>
|
||||||
<h3>👥 还没有客户试用信息</h3>
|
<h3>👥 还没有客户试用信息</h3>
|
||||||
<p>点击上方「添加试用时间」开始管理客户试用</p>
|
<p>点击上方「添加客户」开始管理客户试用</p>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
@ -325,10 +443,12 @@ function renderTrialPeriodsTable() {
|
|||||||
const endTime = formatDateTime(period.endTime);
|
const endTime = formatDateTime(period.endTime);
|
||||||
const createdAt = formatDateTime(period.createdAt);
|
const createdAt = formatDateTime(period.createdAt);
|
||||||
const source = period.source || '';
|
const source = period.source || '';
|
||||||
|
const intendedProduct = period.intendedProduct || '';
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td><strong>${customerName}</strong></td>
|
<td><strong>${customerName}</strong></td>
|
||||||
<td>${source}</td>
|
<td>${source}</td>
|
||||||
|
<td>${intendedProduct}</td>
|
||||||
<td>${statusBadge}</td>
|
<td>${statusBadge}</td>
|
||||||
<td>${startTime}</td>
|
<td>${startTime}</td>
|
||||||
<td>${endTime}</td>
|
<td>${endTime}</td>
|
||||||
@ -410,6 +530,9 @@ function openAddTrialModal() {
|
|||||||
const sourceEl = document.getElementById('trialCustomerSource');
|
const sourceEl = document.getElementById('trialCustomerSource');
|
||||||
if (sourceEl) sourceEl.value = '';
|
if (sourceEl) sourceEl.value = '';
|
||||||
|
|
||||||
|
// Reset intended product
|
||||||
|
setIntendedProductValue('trialIntendedProduct', 'trialIntendedProductOther', '');
|
||||||
|
|
||||||
document.querySelector('input[name="isTrial"][value="true"]').checked = true;
|
document.querySelector('input[name="isTrial"][value="true"]').checked = true;
|
||||||
document.getElementById('trialStartTime').value = '';
|
document.getElementById('trialStartTime').value = '';
|
||||||
document.getElementById('trialEndTime').value = '';
|
document.getElementById('trialEndTime').value = '';
|
||||||
@ -427,6 +550,9 @@ function openEditTrialModal(periodId) {
|
|||||||
const sourceEl = document.getElementById('editTrialCustomerSource');
|
const sourceEl = document.getElementById('editTrialCustomerSource');
|
||||||
if (sourceEl) sourceEl.value = period.source || '';
|
if (sourceEl) sourceEl.value = period.source || '';
|
||||||
|
|
||||||
|
// Set intended product
|
||||||
|
setIntendedProductValue('editTrialIntendedProduct', 'editTrialIntendedProductOther', period.intendedProduct || '');
|
||||||
|
|
||||||
const startDate = new Date(period.startTime);
|
const startDate = new Date(period.startTime);
|
||||||
const endDate = new Date(period.endTime);
|
const endDate = new Date(period.endTime);
|
||||||
|
|
||||||
@ -456,7 +582,6 @@ async function deleteTrialPeriodFromPage(periodId) {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
await loadAllTrialPeriods();
|
await loadAllTrialPeriods();
|
||||||
alert('试用时间删除成功!');
|
|
||||||
} else {
|
} else {
|
||||||
alert('删除试用时间时出错');
|
alert('删除试用时间时出错');
|
||||||
}
|
}
|
||||||
@ -475,6 +600,8 @@ async function createTrialPeriodFromPage() {
|
|||||||
const customerName = inputEl ? inputEl.value.trim() : '';
|
const customerName = inputEl ? inputEl.value.trim() : '';
|
||||||
// Get customer source from input
|
// Get customer source from input
|
||||||
const source = sourceEl ? sourceEl.value.trim() : '';
|
const source = sourceEl ? sourceEl.value.trim() : '';
|
||||||
|
// Get intended product
|
||||||
|
const intendedProduct = getIntendedProductValue('trialIntendedProduct', 'trialIntendedProductOther');
|
||||||
|
|
||||||
const isTrialValue = document.querySelector('input[name="isTrial"]:checked').value;
|
const isTrialValue = document.querySelector('input[name="isTrial"]:checked').value;
|
||||||
const isTrial = isTrialValue === 'true';
|
const isTrial = isTrialValue === 'true';
|
||||||
@ -495,6 +622,7 @@ async function createTrialPeriodFromPage() {
|
|||||||
const formData = {
|
const formData = {
|
||||||
customerName: customerName,
|
customerName: customerName,
|
||||||
source: source,
|
source: source,
|
||||||
|
intendedProduct: intendedProduct,
|
||||||
startTime: new Date(startTime).toISOString(),
|
startTime: new Date(startTime).toISOString(),
|
||||||
endTime: new Date(endTime).toISOString(),
|
endTime: new Date(endTime).toISOString(),
|
||||||
isTrial: isTrial
|
isTrial: isTrial
|
||||||
@ -513,7 +641,6 @@ async function createTrialPeriodFromPage() {
|
|||||||
document.getElementById('addTrialPeriodModal').style.display = 'none';
|
document.getElementById('addTrialPeriodModal').style.display = 'none';
|
||||||
document.getElementById('addTrialPeriodForm').reset();
|
document.getElementById('addTrialPeriodForm').reset();
|
||||||
await loadAllTrialPeriods();
|
await loadAllTrialPeriods();
|
||||||
alert('试用时间添加成功!');
|
|
||||||
} else {
|
} else {
|
||||||
alert('添加试用时间时出错');
|
alert('添加试用时间时出错');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -188,7 +188,6 @@ async function createTrialPeriod() {
|
|||||||
document.getElementById('addTrialPeriodModal').style.display = 'none';
|
document.getElementById('addTrialPeriodModal').style.display = 'none';
|
||||||
document.getElementById('addTrialPeriodForm').reset();
|
document.getElementById('addTrialPeriodForm').reset();
|
||||||
await loadTrialPeriods(customerId);
|
await loadTrialPeriods(customerId);
|
||||||
alert('试用时间添加成功!');
|
|
||||||
} else {
|
} else {
|
||||||
alert('添加试用时间时出错');
|
alert('添加试用时间时出错');
|
||||||
}
|
}
|
||||||
@ -236,6 +235,27 @@ async function updateTrialPeriod() {
|
|||||||
const sourceEl = document.getElementById('editTrialCustomerSource');
|
const sourceEl = document.getElementById('editTrialCustomerSource');
|
||||||
const source = sourceEl ? sourceEl.value.trim() : '';
|
const source = sourceEl ? sourceEl.value.trim() : '';
|
||||||
|
|
||||||
|
// Get intended product value (use the function from trial-periods-page.js if available)
|
||||||
|
let intendedProduct = '';
|
||||||
|
if (typeof getIntendedProductValue === 'function') {
|
||||||
|
intendedProduct = getIntendedProductValue('editTrialIntendedProduct', 'editTrialIntendedProductOther');
|
||||||
|
} else {
|
||||||
|
// Fallback: get values from checkboxes directly
|
||||||
|
const checkboxes = document.querySelectorAll('input[name="editIntendedProduct"]:checked');
|
||||||
|
const otherInput = document.getElementById('editTrialIntendedProductOther');
|
||||||
|
const values = [];
|
||||||
|
checkboxes.forEach(cb => {
|
||||||
|
if (cb.value === '其他') {
|
||||||
|
if (otherInput && otherInput.value.trim()) {
|
||||||
|
values.push(otherInput.value.trim());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
values.push(cb.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
intendedProduct = values.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
// Get isTrial value from radio buttons
|
// Get isTrial value from radio buttons
|
||||||
const isTrialRadio = document.querySelector('input[name="editIsTrial"]:checked');
|
const isTrialRadio = document.querySelector('input[name="editIsTrial"]:checked');
|
||||||
const isTrial = isTrialRadio ? isTrialRadio.value === 'true' : true;
|
const isTrial = isTrialRadio ? isTrialRadio.value === 'true' : true;
|
||||||
@ -247,6 +267,7 @@ async function updateTrialPeriod() {
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
source: source,
|
source: source,
|
||||||
|
intendedProduct: intendedProduct,
|
||||||
startTime: new Date(startTime).toISOString(),
|
startTime: new Date(startTime).toISOString(),
|
||||||
endTime: new Date(endTime).toISOString(),
|
endTime: new Date(endTime).toISOString(),
|
||||||
isTrial: isTrial
|
isTrial: isTrial
|
||||||
@ -271,7 +292,6 @@ async function updateTrialPeriod() {
|
|||||||
if (typeof loadAllTrialPeriods === 'function') {
|
if (typeof loadAllTrialPeriods === 'function') {
|
||||||
await loadAllTrialPeriods();
|
await loadAllTrialPeriods();
|
||||||
}
|
}
|
||||||
alert('试用时间更新成功!');
|
|
||||||
} else {
|
} else {
|
||||||
alert('更新试用时间时出错');
|
alert('更新试用时间时出错');
|
||||||
}
|
}
|
||||||
@ -294,7 +314,6 @@ async function deleteTrialPeriod(periodId) {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
await loadTrialPeriods(currentCustomerId);
|
await loadTrialPeriods(currentCustomerId);
|
||||||
alert('试用时间删除成功!');
|
|
||||||
} else {
|
} else {
|
||||||
alert('删除试用时间时出错');
|
alert('删除试用时间时出错');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,6 +85,7 @@ func (h *TrialPeriodHandler) CreateTrialPeriod(w http.ResponseWriter, r *http.Re
|
|||||||
trialPeriod := models.TrialPeriod{
|
trialPeriod := models.TrialPeriod{
|
||||||
CustomerName: req.CustomerName,
|
CustomerName: req.CustomerName,
|
||||||
Source: req.Source,
|
Source: req.Source,
|
||||||
|
IntendedProduct: req.IntendedProduct,
|
||||||
StartTime: startTime,
|
StartTime: startTime,
|
||||||
EndTime: endTime,
|
EndTime: endTime,
|
||||||
IsTrial: req.IsTrial,
|
IsTrial: req.IsTrial,
|
||||||
|
|||||||
@ -23,7 +23,7 @@ func NewMySQLTrialPeriodStorage() TrialPeriodStorage {
|
|||||||
|
|
||||||
func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, error) {
|
func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, customer_name, COALESCE(source, '') as source, start_time, end_time, is_trial, created_at
|
SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, start_time, end_time, is_trial, created_at
|
||||||
FROM trial_periods
|
FROM trial_periods
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
@ -40,7 +40,7 @@ func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, e
|
|||||||
var isTrial int
|
var isTrial int
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&tp.ID, &tp.CustomerName, &tp.Source, &tp.StartTime,
|
&tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.StartTime,
|
||||||
&tp.EndTime, &isTrial, &tp.CreatedAt,
|
&tp.EndTime, &isTrial, &tp.CreatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -56,7 +56,7 @@ func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, e
|
|||||||
|
|
||||||
func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string) ([]models.TrialPeriod, error) {
|
func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string) ([]models.TrialPeriod, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, customer_name, COALESCE(source, '') as source, start_time, end_time, is_trial, created_at
|
SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, start_time, end_time, is_trial, created_at
|
||||||
FROM trial_periods
|
FROM trial_periods
|
||||||
WHERE customer_name = ?
|
WHERE customer_name = ?
|
||||||
ORDER BY end_time DESC
|
ORDER BY end_time DESC
|
||||||
@ -74,7 +74,7 @@ func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string
|
|||||||
var isTrial int
|
var isTrial int
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&tp.ID, &tp.CustomerName, &tp.Source, &tp.StartTime,
|
&tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.StartTime,
|
||||||
&tp.EndTime, &isTrial, &tp.CreatedAt,
|
&tp.EndTime, &isTrial, &tp.CreatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -90,7 +90,7 @@ func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string
|
|||||||
|
|
||||||
func (ts *mysqlTrialPeriodStorage) GetTrialPeriodByID(id string) (*models.TrialPeriod, error) {
|
func (ts *mysqlTrialPeriodStorage) GetTrialPeriodByID(id string) (*models.TrialPeriod, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, customer_name, COALESCE(source, '') as source, start_time, end_time, is_trial, created_at
|
SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, start_time, end_time, is_trial, created_at
|
||||||
FROM trial_periods
|
FROM trial_periods
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`
|
`
|
||||||
@ -99,7 +99,7 @@ func (ts *mysqlTrialPeriodStorage) GetTrialPeriodByID(id string) (*models.TrialP
|
|||||||
var isTrial int
|
var isTrial int
|
||||||
|
|
||||||
err := ts.db.QueryRow(query, id).Scan(
|
err := ts.db.QueryRow(query, id).Scan(
|
||||||
&tp.ID, &tp.CustomerName, &tp.Source, &tp.StartTime,
|
&tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.StartTime,
|
||||||
&tp.EndTime, &isTrial, &tp.CreatedAt,
|
&tp.EndTime, &isTrial, &tp.CreatedAt,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -123,8 +123,8 @@ func (ts *mysqlTrialPeriodStorage) CreateTrialPeriod(trialPeriod models.TrialPer
|
|||||||
}
|
}
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO trial_periods (id, customer_name, source, start_time, end_time, is_trial, created_at)
|
INSERT INTO trial_periods (id, customer_name, source, intended_product, start_time, end_time, is_trial, created_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`
|
`
|
||||||
|
|
||||||
isTrial := 0
|
isTrial := 0
|
||||||
@ -133,8 +133,8 @@ func (ts *mysqlTrialPeriodStorage) CreateTrialPeriod(trialPeriod models.TrialPer
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err := ts.db.Exec(query,
|
_, err := ts.db.Exec(query,
|
||||||
trialPeriod.ID, trialPeriod.CustomerName, trialPeriod.Source, trialPeriod.StartTime,
|
trialPeriod.ID, trialPeriod.CustomerName, trialPeriod.Source, trialPeriod.IntendedProduct,
|
||||||
trialPeriod.EndTime, isTrial, trialPeriod.CreatedAt,
|
trialPeriod.StartTime, trialPeriod.EndTime, isTrial, trialPeriod.CreatedAt,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -158,6 +158,9 @@ func (ts *mysqlTrialPeriodStorage) UpdateTrialPeriod(id string, updates models.U
|
|||||||
if updates.Source != nil {
|
if updates.Source != nil {
|
||||||
existing.Source = *updates.Source
|
existing.Source = *updates.Source
|
||||||
}
|
}
|
||||||
|
if updates.IntendedProduct != nil {
|
||||||
|
existing.IntendedProduct = *updates.IntendedProduct
|
||||||
|
}
|
||||||
if updates.StartTime != nil {
|
if updates.StartTime != nil {
|
||||||
startTime, err := time.Parse(time.RFC3339, *updates.StartTime)
|
startTime, err := time.Parse(time.RFC3339, *updates.StartTime)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -176,7 +179,7 @@ func (ts *mysqlTrialPeriodStorage) UpdateTrialPeriod(id string, updates models.U
|
|||||||
|
|
||||||
query := `
|
query := `
|
||||||
UPDATE trial_periods
|
UPDATE trial_periods
|
||||||
SET customer_name = ?, source = ?, start_time = ?, end_time = ?, is_trial = ?
|
SET customer_name = ?, source = ?, intended_product = ?, start_time = ?, end_time = ?, is_trial = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -186,8 +189,8 @@ func (ts *mysqlTrialPeriodStorage) UpdateTrialPeriod(id string, updates models.U
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err = ts.db.Exec(query,
|
_, err = ts.db.Exec(query,
|
||||||
existing.CustomerName, existing.Source, existing.StartTime, existing.EndTime,
|
existing.CustomerName, existing.Source, existing.IntendedProduct,
|
||||||
isTrial, id,
|
existing.StartTime, existing.EndTime, isTrial, id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -7,6 +7,7 @@ type TrialPeriod struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
CustomerName string `json:"customerName"` // 直接存储客户名称
|
CustomerName string `json:"customerName"` // 直接存储客户名称
|
||||||
Source string `json:"source"` // 客户来源
|
Source string `json:"source"` // 客户来源
|
||||||
|
IntendedProduct string `json:"intendedProduct"` // 意向产品
|
||||||
StartTime time.Time `json:"startTime"`
|
StartTime time.Time `json:"startTime"`
|
||||||
EndTime time.Time `json:"endTime"`
|
EndTime time.Time `json:"endTime"`
|
||||||
IsTrial bool `json:"isTrial"`
|
IsTrial bool `json:"isTrial"`
|
||||||
@ -17,6 +18,7 @@ type TrialPeriod struct {
|
|||||||
type CreateTrialPeriodRequest struct {
|
type CreateTrialPeriodRequest struct {
|
||||||
CustomerName string `json:"customerName"` // 直接使用客户名称
|
CustomerName string `json:"customerName"` // 直接使用客户名称
|
||||||
Source string `json:"source"` // 客户来源
|
Source string `json:"source"` // 客户来源
|
||||||
|
IntendedProduct string `json:"intendedProduct"` // 意向产品
|
||||||
StartTime string `json:"startTime"`
|
StartTime string `json:"startTime"`
|
||||||
EndTime string `json:"endTime"`
|
EndTime string `json:"endTime"`
|
||||||
IsTrial bool `json:"isTrial"`
|
IsTrial bool `json:"isTrial"`
|
||||||
@ -26,6 +28,7 @@ type CreateTrialPeriodRequest struct {
|
|||||||
type UpdateTrialPeriodRequest struct {
|
type UpdateTrialPeriodRequest struct {
|
||||||
CustomerName *string `json:"customerName,omitempty"`
|
CustomerName *string `json:"customerName,omitempty"`
|
||||||
Source *string `json:"source,omitempty"` // 客户来源
|
Source *string `json:"source,omitempty"` // 客户来源
|
||||||
|
IntendedProduct *string `json:"intendedProduct,omitempty"` // 意向产品
|
||||||
StartTime *string `json:"startTime,omitempty"`
|
StartTime *string `json:"startTime,omitempty"`
|
||||||
EndTime *string `json:"endTime,omitempty"`
|
EndTime *string `json:"endTime,omitempty"`
|
||||||
IsTrial *bool `json:"isTrial,omitempty"`
|
IsTrial *bool `json:"isTrial,omitempty"`
|
||||||
|
|||||||
74
tools/add_intended_product_column.go
Normal file
74
tools/add_intended_product_column.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Database connection parameters
|
||||||
|
host := os.Getenv("DB_HOST")
|
||||||
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
|
}
|
||||||
|
user := os.Getenv("DB_USER")
|
||||||
|
if user == "" {
|
||||||
|
user = "root"
|
||||||
|
}
|
||||||
|
password := os.Getenv("DB_PASSWORD")
|
||||||
|
if password == "" {
|
||||||
|
password = ""
|
||||||
|
}
|
||||||
|
dbName := os.Getenv("DB_NAME")
|
||||||
|
if dbName == "" {
|
||||||
|
dbName = "crm_db"
|
||||||
|
}
|
||||||
|
port := os.Getenv("DB_PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "3306"
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||||
|
user, password, host, port, dbName)
|
||||||
|
|
||||||
|
db, err := sql.Open("mysql", dsn)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to connect to database:", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Check if column exists
|
||||||
|
var columnExists int
|
||||||
|
err = db.QueryRow(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = ?
|
||||||
|
AND TABLE_NAME = 'trial_periods'
|
||||||
|
AND COLUMN_NAME = 'intended_product'
|
||||||
|
`, dbName).Scan(&columnExists)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to check column existence:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if columnExists > 0 {
|
||||||
|
log.Println("✅ Column 'intended_product' already exists in trial_periods table")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the intended_product column
|
||||||
|
_, err = db.Exec(`
|
||||||
|
ALTER TABLE trial_periods
|
||||||
|
ADD COLUMN intended_product VARCHAR(255) DEFAULT '' AFTER source
|
||||||
|
`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to add intended_product column:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("✅ Successfully added 'intended_product' column to trial_periods table")
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user